paloma blog

NWエンジニアやってます。技術の備忘など。Pythonもちょっと。タイトルは好きなカクテルから。

スリーカードポーカーで勝つことはできるのか フォールド処理組み込み編

前回降りる手札判定が出来たところでシミュレータに組み込みます。

シミュレータの流れは

  1. チップベット ($1000スタート、アンティ・ペアプラスは$10固定)
  2. カード配布
  3. 手札判定
  4. 勝負
  5. 払い戻し

となっていますがフォールドの手順をこう組み込みます。

  1. チップベット ($1000スタート、アンティ・ペアプラスは$10固定)
  2. カード配布
  3. 手札判定
    1. フォールドする手札か判定
    2. フォールドならチップ分を引いて終了
  4. 勝負
  5. 払い戻し

こうみるとシンプルで簡単すぎますね。
はやくやっときゃよかった。

手札チェック

Booleanを返すように前回より少し変更してます。

def Foldcheck(hand):
    if pokerapp.Handcheck(hand).result()[0][0] == 1:
        fhand = pokerapp.Handcheck(hand).result()[1]

        if fhand[2] <= 12 and fhand[1] <= 6 and fhand[0] <= 4:
            return True

        else:
            return False
    else:
        return False

引数に手札を与えると、降りる手札の場合Trueを返します。
Trueなら以降の処理は行わずに終了し、賭けたチップを放棄します。

>>> Foldcheck(player)
False
>>> Foldcheck(player)
True

動かしてみる

組み込んだところで動かしてみます。

masashi@PC-ubuntu:~/Three-card-poker$ python3 simulater.py 
You are first given $1,000.
Three card poker simulator start.
The simulator trials 1,000 times.
You'll bet ante $10 and pair plus $10 all the time.
You fold the game if you hand less than Q,6,4 High card.

You could play the game 1000 times.

=== Probabliry of winning or losing ===

Player 500 wins. (50.0%)
Dealer 333 wins. (33.3%)
Draw 0 times. (0.0%)
Fold 167 times. (16.7%)

=== Percentage of Players all hands ===

High card!            58.0%
One Pair!             16.4%
Flash!                 5.7%
Straight!              2.7%
Three of a kind!       0.3%
Straight Flash!        0.2%

=== Percentage of Players win(500 times) hands ===

High card!            56.0%
One Pair!             27.0%
Flash!                10.8%
Straight!              5.2%
Three of a kind!       0.6%
Straight Flash!        0.4%

You finally got $230

弱い手札は降りるようにしたところで結果はあまり変わりませんね。
こう見ると意外とフォールド率が高いです。

目的は勝ち越せるかなので数回回してみましょう。

masashi@PC-ubuntu:~/Three-card-poker$ for a in {1..10} ; do python3 simulater.py | tail -n1 ; done
You finally got $10
You finally got $10
You finally got $20
You finally got $20
You finally got $3,330
You finally got $1,500
You finally got $20
You finally got $1,760
You finally got $10
You finally got $20

1000 × 10回試行して終了時の平均$670ですね。
$1000スタートなので負け越しです…。

結論 やはりうまい話はない

勝ち越せるケースはあるので短期で勝つことは出来ますが長期的にやり続けると負けてしまいますね。
スリーカードポーカーもシンプルながらも良くできています。

シミュレータのお陰でリアルのチップを無駄遣いせずにすみましたw

じゃあ逆に短期決戦で損益の分岐を見極めれば勝ち越せる...?
この研究はまた今度ですね。

前編

スリーカードポーカーで勝つことはできるのか 降りる手札判別編 - paloma blog

スリーカードポーカーで勝つことはできるのか 降りる手札判別編

スリーカードポーカーのゲームを作ってからGUI化に取り掛かりましたがすっかり止まっています。

ボタン処理が難しいんだもん。
作りながら覚えればいいのですが、ゲームもやるのも好きなのでそっちばかりやってしまいなかなか進みません。

GUI化はいったんおいておきまして今回はシミュレータの改良です。
ゲームと合わせてシミュレータも作ったのですが勝負が全ツッパなので高い確率で資金が底をついてしまいます。

$1000持ちでスタートしていますが数百回で資金がなくなります。

masashi@PC-ubuntu:~/Three-card-poker$ python3 simulater.py 
You are first given $1,000.
Three card poker simulator start.
The simulator trials 1,000 times.
You'll bet ante $10 and pair plus $10 all the time.

You could play the game 289 times.

=== Probabliry of winning or losing ===

Player 161 wins. (55.7%)
Dealer 128 wins. (44.3%)
Draw 0 times. (0.0%)

=== Percentage of Players all hands ===

High card!          76.471%
One Pair!           16.609%
Flash!               4.152%
Straight!            2.768%

=== Percentage of Players win(161 times) hands ===

High card!          63.975%
One Pair!           24.224%
Flash!               6.832%
Straight!            4.969%

You finally got $10

10回に1回位は勝ち越せてたかな。

チップがかかっている以上無駄な消費を減らすためにオリ、いわゆるフォールドの処理を追加しようと思います。

スリーカードポーカーにも定石があり、Q、6、4以下のハイカードは降りるべきとされています。

なんでそうすべきかの説明文を読めていないので根拠がわかりませんがいろんなサイトで出てくるので一旦これに乗っかります。

降りる手札を判別する

イカードの中から特定の数以下の手札を判別します。

本当は手札評価はビット演算でスマートにやるべきなんでしょうがパターンもそんなに無いので組み合わせでぱぱっとやります。

Q、6、4以下の手札を出す関数です。
条件分を何パターンか試しましたが、andで全条件を判定してしまうのがうまくいきました。

pokerappは自作クラスです。
トランプのデッキ作成からカード配布、役判定まで作成済みなので今回の処理も簡単に作れました。
関数を使いまわせて便利ですね。

def Foldcheck():
    deck = pokerapp.Deck()
    deck.shuffle()
    player = pokerapp.Handout(deck)
    if pokerapp.Handcheck(player).get_role()[0][0] == 'High card!':
        fhand = pokerapp.Judge(player)[1]
        if fhand[2] <= 12 and fhand[1] <= 6 and fhand[0] <= 4:
            print(fhand, 'fold')

実践

ちょっと回してみます。

>>> for _ in range(101): Foldcheck()
... 
[3, 5, 12] fold
[2, 3, 11] fold
[2, 6, 11] fold
[2, 3, 5] fold
[4, 5, 11] fold
[2, 3, 10] fold
[2, 3, 7] fold
[4, 6, 10] fold
[2, 3, 5] fold
[2, 3, 7] fold
[2, 3, 9] fold
[4, 6, 11] fold
[2, 3, 8] fold
[2, 3, 10] fold

100回回して全部Q、6、4以下の降りる手札になってますね。
勝負手は出力されないのでこれでよし。

降りる手判別の機能が出来たのでシミュレータに組み込みます。
ここは次回で。

本シリーズ

スリーカードポーカーゲームを作りたい やっとゲームになった編 - paloma blog

スリーカードポーカーゲームを作りたい シミュレータ作る編 - paloma blog

スリーカードポーカーで勝つことはできるのか - paloma blog

スリーカードポーカーで勝つことはできるのか 実践編 - paloma blog

リポジトリ

github.com

ダミーファイルとscpでNW速度を計測

今やってる案件で帯域制限を行うのですが、計測の試験をしなければいけません。
事前に家の環境で試験の練習をしようと思います。

環境

本番はLinuxサーバなので家でもLinuxでやります。
Windows上のWSL(Ubuntu)から物理のUbuntuへ転送します。

家の構成は以下です。

[WSL Ubuntu] -- [RT] -- [Ubuntu]

簡単な構成です。
家では特にNWの制限は行っていません。

WSL

昔作ったWSL1の環境のままです。 ubuntuも16と古いですね。

masashi@DESKTOP-986MNSO:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.4 LTS
Release:        16.04
Codename:       xenial

WSL上からではインターフェース速度が見えませんでした。

masashi@DESKTOP-986MNSO:~$ ip a
15: eth0: <BROADCAST,MULTICAST,UP> mtu 1500 group default qlen 1
    link/ether 6c:62:6d:e9:5f:5f
    inet 192.168.0.23/24 brd 192.168.0.255 scope global dynamic
       valid_lft 3596sec preferred_lft 3596sec
    inet6 fe80::9a7:f03b:74e7:e259/64 scope link dynamic
       valid_lft forever preferred_lft forever
1: lo: <LOOPBACK,UP> mtu 1500 group default qlen 1
    link/loopback 00:00:00:00:00:00
    inet 127.0.0.1/8 brd 127.255.255.255 scope global dynamic
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host dynamic
       valid_lft forever preferred_lft forever
masashi@DESKTOP-986MNSO:~$
masashi@DESKTOP-986MNSO:~$ ethtool eth0
Settings for eth0:
Cannot get device settings: Invalid argument
Cannot get wake-on-lan settings: Invalid argument
Cannot get message level: Invalid argument
Cannot get link status: Invalid argument
No data available

ホスト側で確認すると100Mでした。
アダプタ名にGbEってあるのに何故か100Mになります。
RT側のインターフェースが一部100Mなのでしょうか。

まあこのままでいきます。

PS C:\Users\tsuru> get-netadapter

Name                      InterfaceDescription                    ifIndex Status       MacAddress             LinkSpeed
----                      --------------------                    ------- ------       ----------             ---------
イーサネット              Realtek PCIe GbE Family Controller           15 Up           6C-62-6D-E9-5F-5F       100 Mbps
Ubuntu

こっちはデスクトップのUbuntu20です。

masashi@PC-ubuntu:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:        20.04
Codename:       focal

100Mに引っ張られるので有効ではないですが、こっちは1G出てますね。

masashi@PC-ubuntu:~$ sudo ethtool enp2s0 | grep -A1 Speed:
        Speed: 1000Mb/s
        Duplex: Full

ダミーファイル作成

WSL Ubuntuでダミーファイルを作成します。

よくあるddコマンドを使います。
転送速度の計測にはある程度のサイズが必要なので今回は1G作成します。

masashi@DESKTOP-986MNSO:~$ dd if=/dev/zero of=dummyfile bs=1M count=1000
1000+0 records in
1000+0 records out
1048576000 bytes (1.0 GB, 1000 MiB) copied, 1.35842 s, 772 MB/s
masashi@DESKTOP-986MNSO:~$ ls -l dummyfile
-rw-r--r-- 1 masashi masashi 1048576000 Jun  6 15:14 dummyfile

速度計測

ファイル転送速度の計測ですが、scpを使います。
サーバにはsshを入れるし、scpは転送速度が表示されるのでちょうどいいですね。
iperfも有名ですが、別途インストールが必要なので今回は行いません。

早速実行してみます。

masashi@DESKTOP-986MNSO:~$ scp dummyfile masashi@192.168.0.10:/home/masashi/
masashi@192.168.0.10's password:
dummyfile                      100% 1000MB  11.2MB/s   01:29

転送速度は11.2MByte/秒でした。
本番ではここが実際の絞った値になっていればOKですね。

送信側が100Mインターフェースですが、bit計算なのでbyteに直すと
1000000 / 8 = 125000
で毎秒12.5MByte送信できる計算になります。
11.2はいちおう近似値でOKかな…

まとめ

速度計測用のファイル作成とscpでの速度確認でした。
この手の記事はネットに転がっているので改めてやることも無かった気がしますが、
自分用メモで置いておきます。

おまけ

ダミーファイル作成はddコマンドを使いましたがfallocateコマンドでも作成できるようです。
こちらのコマンドはディスク領域を予約するのみでIOが発生しないそうなので作成が早いです。

1G指定でもブロックサイズの都合もありちょっとずれますね。

masashi@DESKTOP-986MNSO:~$ fallocate -l 1g dummyfile2
masashi@DESKTOP-986MNSO:~$ ls -l dummyfile*
-rw-r--r-- 1 masashi masashi 1048576000 Jun  6 15:14 dummyfile
-rw-r--r-- 1 masashi masashi 1073741824 Jun  6 17:05 dummyfile2

領域予約で作ったファイルだと計測が本来の値からずれそうなので上記の検証には使いませんでしたが、計測してみると同じ結果になりました。
マニュアル見たらfallocateも0埋めをしているようです。
ddも/dev/zeroから作ったので同じ様なものが作成されたのでしょうか?

masashi@DESKTOP-986MNSO:~$ scp dummyfile2 masashi@192.168.0.10:/home/masashi/
masashi@192.168.0.10's password:
dummyfile2                     100% 1024MB  11.3MB/s   01:31

fallocateコマンドもダミーファイル作成はいいとして、本来の用途が良くわかりません。
調べる価値ありですね。

参考サイト

Linuxでサイズ指定してダミーファイルを作成する方法 - conf t

Man page of DD

Man page of FALLOCATE

LXDコンテナのカスタムイメージを作成する

またまたFireflyの話です。
LXD環境で運用していますが一度LXDプロセスが壊れてしまって作り直しました。
また同じことが会ってもすぐ復旧できるようにバックアップを取っておきたいところです。

リカバリだけではなくバージョンアップ用の土台にもなりますので今回はコンテナイメージの作成を行ってみます。

バックアップ内容

以下の順で作成します。

snapshotからコピーを取るのはyoutubeで動画があったので参考にしました。
わざわざ取らなくても良かったかもしれません。

  1. snapshot作成
  2. コンテナのコピー
  3. コピーからイメージ作成

LXDバージョン

ubuntuデスクトップ上で動かしてます。

masashi@PC-ubuntu:~$ lxc --version
4.13

snapshot作成

今運用しているFireflyコンテナのsnapshotを取ります。
停止してコマンド打つだけです。

masashi@PC-ubuntu:~$ lxc stop firefly
masashi@PC-ubuntu:~$ lxc snapshot firefly 
masashi@PC-ubuntu:~$ lxc info firefly
Name: firefly
Location: none
Remote: unix://
Architecture: x86_64
Created: 2021/02/04 14:31 UTC
Status: Stopped
Type: container
Profiles: default
Snapshots:
  snap0 (taken at 2021/05/08 01:54 UTC) (stateless)

コンテナのコピー

イメージ作成用のコンテナを作ります。
本番コンテナからでも良かったのですが一応コピーします。

masashi@PC-ubuntu:~$ lxc copy firefly/snap0 firefly-backup
masashi@PC-ubuntu:~$ lxc list
+----------------+---------+---------------------+-----------------------------------------------+-----------+-----------+
|      NAME      |  STATE  |        IPV4         |                     IPV6                      |   TYPE    | SNAPSHOTS |
+----------------+---------+---------------------+-----------------------------------------------+-----------+-----------+
| debian         | RUNNING | 10.28.57.205 (eth0) | fd42:24ab:b835:ae42:216:3eff:fe98:2560 (eth0) | CONTAINER | 0         |
+----------------+---------+---------------------+-----------------------------------------------+-----------+-----------+
| dokuwiki       | RUNNING | 10.28.57.198 (eth0) | fd42:24ab:b835:ae42:216:3eff:fe2e:4f6a (eth0) | CONTAINER | 0         |
+----------------+---------+---------------------+-----------------------------------------------+-----------+-----------+
| firefly        | STOPPED |                     |                                               | CONTAINER | 1         |
+----------------+---------+---------------------+-----------------------------------------------+-----------+-----------+
| firefly-backup | RUNNING | 10.28.57.192 (eth0) | fd42:24ab:b835:ae42:216:3eff:feca:aaf9 (eth0) | CONTAINER | 0         |
+----------------+---------+---------------------+-----------------------------------------------+-----------+-----------+
| lxd-proxy      | RUNNING | 10.28.57.177 (eth0) | fd42:24ab:b835:ae42:216:3eff:fecd:d475 (eth0) | CONTAINER | 0         |
+----------------+---------+---------------------+-----------------------------------------------+-----------+-----------+

できました。

イメージ作成前の準備

イメージ用のコンテナはDBを空にしておきたいのでスタートしてDBを初期化しておきます。

masashi@PC-ubuntu:~$  lxc start firefly-backup 

コンテナ内でDBを初期化します。
php artisanコマンドを使いますが、Fireflyの話なので割愛。

masashi@PC-ubuntu:~$  lxc exec firefly-backup bash

コピーからイメージ作成

再度コンテナを止めてイメージを作成します。

masashi@PC-ubuntu:~$ lxc stop firefly-backup 
masashi@PC-ubuntu:~$ lxc publish firefly-backup --alias firefly-image
Instance published with fingerprint: 754669c7e39f9a7cbb22a1c72ef8935b18a75f3c72c41d2f88e8db29305ae071

イメージ作成ができました。
ここからデプロイすればいつでもFireflyが復旧できますね。

masashi@PC-ubuntu:~$ lxc image list
+---------------+--------------+--------+---------------------------------------------+--------------+-----------+----------+------------------------------+
|     ALIAS     | FINGERPRINT  | PUBLIC |                 DESCRIPTION                 | ARCHITECTURE |   TYPE    |   SIZE   |         UPLOAD DATE          |
+---------------+--------------+--------+---------------------------------------------+--------------+-----------+----------+------------------------------+
| firefly-image | 754669c7e39f | no     | Ubuntu 20.04 LTS server (20210201)          | x86_64       | CONTAINER | 873.23MB | May 8, 2021 at 2:32am (UTC)  |
+---------------+--------------+--------+---------------------------------------------+--------------+-----------+----------+------------------------------+
|               | 90e2d54028dd | no     | ubuntu 20.04 LTS amd64 (release) (20210429) | x86_64       | CONTAINER | 361.08MB | Apr 30, 2021 at 8:52am (UTC) |
+---------------+--------------+--------+---------------------------------------------+--------------+-----------+----------+------------------------------+

ホスト環境にコピー

このままだとLXD環境にはイメージが残っていますが、また壊れたら回収できなくなるので今のうちにホスト側にもコピーしておきます。

masashi@PC-ubuntu:~$ lxc image export firefly-image .
masashi@PC-ubuntu:~$ ls -ltrh
…
-rw-rw-r--  1 masashi masashi 874M  58 12:26  754669c7e39f9a7cbb22a1c72ef8935b18a75f3c72c41d2f88e8db29305ae071.tar.gz

fingerprintの名前でファイルコピーができました。
結構サイズが大きいのでオンラインストレージに逃がすのはちょっと気が引けますね。
家のNASにでも退避させておきましょう。
ファイル名も分かりやすいように変えておきます。

まとめ

今回はカスタムコンテナのイメージ作成を行いました。
デプロイの確認はしていませんが別の機会に試したいと思います。

難しいことはやってませんが、LXDもナレッジが結構あるのでイメージ作成も詰まることなくできました。

別のLXDホストへデプロイ出来るかといった課題は残っていますが今回はここまでです。
しかしコンテナの技術は触ってて面白いですね。

AndroidからpfSenseにIPsec接続して内部NWにアクセスする

自宅でFirefly iiiを本格的に運用し始めたのですが、支払いはすぐに記帳したいところです。

マネートラッキングをFireflyに一本化する API投入編 - paloma blog

特に旅行中など自宅にいないときに感じます。
メモっておけば済む話ですが、VPNでも張って外からサーバにアクセス出来たら便利ですよね。

現在家に常時稼働したサーバはないのですが、試しにVirtual BOXで環境を作ってみました。

構成図

以前紹介したものから色々アップデートしました。
今はこんな感じです。

f:id:paloma69:20210505155946p:plain

環境

こちらも以前作った環境を使います。 ノートPC内にVirtual BOXで環境を作っています。

nwdiagで自宅ネットワーク構成図を自動アップデート - paloma blog

今回ちょっと環境変更したので構成図は上記が最新です。

やること

いったん自宅内での構築ということでスマホ(Mobile)からpfSenceにVPNを張って内部NW(VirtualBOXNW)のFirefly検証機にアクセスしたいと思います。
Firefly検証機はVirtual BOXの内部ネットワークに接続しており、pfSenseしか出口が無いのでHomeNWからは直接アクセスできません。

Internet経由のアクセスを想定して、FWポリシーを許可するのではなくpfSenseとVPNを張ってそれ経由でアクセスさせます。

VPNをどう張るか

仕事でやったことあるのはSSL-VPNIPsecです。
スマホからならSSL-VPNのほうが簡単そうですが、openVPN等のツールを入れないといけないのでAndroidに標準についているIPsecで環境を作ります。
(iPhoneユーザではないのでiOSはわかりません)

pfSenceの構築

VPN用のマシンについてNWエンジニアとしてはメーカーのアプライアンスを使いたいところですが、すぐに導入出来てわりと構築が簡単なものがいいということでpfSenceを使います。 オープンソースFirewallという位置づけながら高機能で触っていて面白いです。

基本的な構築は割愛してIPsec部分だけ紹介します。

ユーザ認証方式

IPsecはSite-to-siteしか業務の経験がないのですがいろいろ検索した結果
IPsec + xauthを使います。

登録したユーザ名の認証が通ったらVPNが張れます。
SSL-VPNならこの方式でやったことあります。
当然かもしれませんがIPsecもできるんですね。

手順は以下の通りです。

pfSense Configuration Recipes — IPsec Remote Access VPN Example Using IKEv1 with Xauth | pfSense Documentation

各パラメータ

アルゴリズムはそんなに強くないですが、Android側の仕様のようなのでドキュメント通りに設定しています。

f:id:paloma69:20210505160136p:plain

あとモバイル端末直なのでアグレッシブモードにするのも肝ですね。

Mobile Client Supportは有効化しておかないと繋がりませんでした。

f:id:paloma69:20210505160209p:plain

  • Pre Shered Key

証明書認証もありますがIPsecといったらPSKですよね。
識別IDとキーは一応隠します。

f:id:paloma69:20210505160255p:plain

  • FWルール

家のNWなのでいったん全空けです。

f:id:paloma69:20210505160319p:plain

  • ユーザ

IPsecのユーザ認証用に作成。
L2TP/IPsecで一回失敗したのでxauthにしました。
(今振り返るとL2TP前のIPsecで何かこけてたと思う)

f:id:paloma69:20210505160343p:plain

AndroidからVPN接続

これでIPsecの準備は完了したのでAndroidからつなげてみます。
設定アプリから行います。

f:id:paloma69:20210505160729p:plain

プロファイル名、Pre Shered Key、認証ユーザを設定します。
アルゴリズムは選択できないのでやはり仕様で決まっている様ですね。

プロファイルができましたので接続してみます。

f:id:paloma69:20210505160852p:plain

f:id:paloma69:20210505160928p:plain

一応鍵マーク付きましたね。
ギガぞうのプロファイルはお気になさらず。

Firefly検証機へアクセス

Androidはルータ向けのデフォルトゲートウェイしかなく内部NWの10.0.0.0/24へはアクセスできません。
接続できればVPN接続もOKということですね。

f:id:paloma69:20210505161038p:plain

OK!

AndroidからではNWの詳細が確認できませんが検証サーバへアクセス出来ています。
テスト用に資産100万を入力したきりですね。

あとpfSence上ではIPsecステータスが有効になっています。

f:id:paloma69:20210505161259p:plain

まとめ

というわけでAndroidとpfSenseでIPsecを張って内部サーバへアクセスすることができました。
手順通りなのであまり考える余地はありませんでしたがその分すんなり作れたので良しとします。

買う買う言いながら全然ホームサーバ用のマシンを買っていませんが、環境は作ったのでOVAかコンフィグのimportでいつでも移動できるようになりました。

もう少しVPN環境について再考するかもしれませんが、今回やりたいことはできました。
今年のGWは1~3日までキャンプへ行っていてその時本件を思いついたのですが、一応GW内に接続まで確認できました。
今年はカレンダー通りでそこまでの連休は取れませんでしたが、とりあえず期間内に1本記事を書けてよかったです。

本当は外からアクセスするまでやりたかったですが、いったんここまでですね。

マネートラッキングをFireflyに一本化する API投入編

前回Firefly IIIのAPIを確認したところでクレジット支払いのcsvファイルを変換して投入したいと思います。

目的のおさらい

クレジットの明細csvファイルをPOST用のフォーマットに変換してAPI経由で投入する

カテゴリー整理

クレジットカードは翌月の支払いになるのでいつの支払い扱いにすればいいか迷いますが、 マネートラッキングということでシンプルに支払いが来た月の支出にします。

私はほとんどキャッシュレス生活なので支払いのカテゴリを下記の形に分けます。

  • スーパー
  • サブスク
  • 買い物(食材以外)
  • 交際費(飲み屋系)
  • Suica払い
  • その他突発的は払いは分類しない

POSTフォーマット

transactionsが資金移動の項目です。

公式ドキュメントにはフルセット書いてありますが、issueを探したら以下の形でOKでした。
リクエストの必須パラメータが書いてないので分かりずらいですね。
No parametersなので何でもいいのかな。

{
  "transactions": [
    {
      "type": "withdrawal",
      "date": "2021-03-26T00:00:00+09:00", # 支払日
      "amount": "xxxx", # 金額
      "description": "xxxx", # 支払い項目
      "currency_id": 16, # JPY
      "category_id": xxxx, # カテゴリ
      "source_id": 5, # トラッキング用アカウント 
      "destination_id": 36 # クレジット払いのアカウント
    }
  ]
}

csvの変換コード

4月分の請求はまだ来ていないので3月分で作ってみます。

csvの内容

クレジットの明細は以下の形です。

2021/01/31,インターネットイニシアティブ,1829,,,,
2021/01/31,ヤフージャパン,508,,,,
2021/02/01,J:COM サービス利用料,5729,,,,
2021/02/03,JR東日本モバイルSuica,2000,,,,
2021/02/04,イトーヨーカドー 食品,3237,,,,イトーヨーカドーXXX店 食料品
2021/02/05,JR東日本モバイルSuica,2000,,,,
2021/02/06,イオンリテール,1282,,,,
2021/02/06,イオンリテール,4121,,,,
2021/02/10,JR東日本モバイルSuica,3000,,,,
…
pythonで変換

何で作るか迷いましたがとりあえず慣れているpythonで作ります。
csvから読み込んで出力は何度もやっているので私としてはおなじみの形です。
テンプレに明細、金額、カテゴリを渡しているだけです。

分類分けが長いので抜粋版です。

import csv

with open('202103.csv', 'r') as f: 
    reader = csv.reader(f)
    data = [x for x in reader]

text = '"{{ "transactions": [ {{ "type": "withdrawal", "date": "2021-03-26T00:00:00+09:00", "amount": "{}", \
"description": "{}", "currency_id": 16, "category_id": {}, "source_id": 5, "destination_id": 36 }} ] }}"'

for a in data[1:-3]: # data内のヘッダ、フッタは渡さない
  if 'ヤフーかんたん決済' in a[1]:
      print(text.format(a[2],' '.join([a[0],a[1]]),9))
  elif 'JR東日本モバイルSuica' in a[1]:
      print(text.format(a[2],' '.join([a[0],a[1]]),7))
  elif 'イオンリテール' in a[1]:
      print(text.format(a[2],' '.join([a[0],a[1]]),6))
  elif 'イトーヨーカドー' in a[1]:
      print(text.format(a[2],' '.join([a[0],a[1]]),6))
  elif 'インターネットイニシアティブ' in a[1]:
      print(text.format(a[2],' '.join([a[0],a[1]]),8))
  elif 'AMAZON.CO.JP' in a[1]:
      print(text.format(a[2],' '.join([a[0],a[1]]),9))
  elif 'DIGITALOCEAN.COM' in a[1]:
      print(text.format(a[2],' '.join([a[0],a[1]]),8))
  elif 'NETFLIX.COM' in a[1]:

出力する

ワンライナーで出力されて記事内だと分かりづらいので改行してます。

masashi@DESKTOP-986MNSO:/mnt/c/Users/tsuru/tools/expenses$ python3 transactionapi.py
"{ "transactions": [ { "type": "withdrawal", "date": "2021-03-26T00:00:00+09:00", "amount": "1829", \
"description": "2021/01/31 インターネットイニシアティブ", "currency_id": 16, "category_id": 8, "source_id": 5, "destination_id": 36 } ] }"
"{ "transactions": [ { "type": "withdrawal", "date": "2021-03-26T00:00:00+09:00", "amount": "508", \
"description": "2021/01/31 ヤフージャパン", "currency_id": 16, "category_id": 8, "source_id": 5, "destination_id": 36 } ] }"
"{ "transactions": [ { "type": "withdrawal", "date": "2021-03-26T00:00:00+09:00", "amount": "5729", ¥
"description": "2021/02/01 J:COM サービス利用料", "currency_id": 16, "category_id": 8, "source_id": 5, "destination_id": 36 } ] }"

いいですね。

API用のコマンドと結合

POST用にcurlコマンドと結合します。
後々何で作るかは考えますが、とりあえずはshellで回して作成します。

masashi@DESKTOP-986MNSO:/mnt/c/Users/tsuru/tools/expenses$ python3 transactionapi.py | while read line ; do echo "curl -X POST http://192.168.0.10/api/v1/transactions -H \"accept: application/json\" -H \"Authorization: Bearer \$(cat 'firefl
y token.txt')\" -H \"Content-Type: application/json\" -d ${line}" ; done
curl -X POST http://192.168.0.10/api/v1/transactions -H "accept: application/json" -H "Authorization: Bearer $(cat 'firefly token.txt')" -H "Content-Type: application/json" -d "{ "transactions": [ { "type": "withdrawal", "date": "2021-03-26T00:00:00+09:00", "amount": "1829", "description": "2021/01/31 インターネットイニシアティブ", "currency_id": 16, "category_id": 8, "source_id": 5, "destination_id": 36 } ] }"
curl -X POST http://192.168.0.10/api/v1/transactions -H "accept: application/json" -H "Authorization: Bearer $(cat 'firefly token.txt')" -H "Content-Type: application/json" -d "{ "transactions": [ { "type": "withdrawal", "date": "2021-03-26T00:00:00+09:00", "amount": "508", "description": "2021/01/31 ヤフージャパン", "currency_id": 16, "category_id": 8, "source_id": 5, "destination_id": 36 } ] }"

OK。 いい感じです。
そのまま流すのは怖いのでいったんテキストに吐いてから流していきます。

サンプルとして1つ張ります。

masashi@DESKTOP-986MNSO:/mnt/c/Users/tsuru/tools/expenses$ curl -X POST http://192.168.0.10/api/v1/transactions -H "accept: application/json" -H "Authorization: Bearer $(cat 'firefly token.txt')" \
-H "Content-Type: application/json" -d '{ "transactions": [ { "type": "withdrawal", "date": "2021-03-26T00:00:00+09:00", "amount": "508", \
"description": "2021/01/31 ヤフージャパン", "currency_id": 16, "category_id": 8, "source_id": 5, "destination_id": 36 } ] }'
{"data":{"type":"transactions",
"id":"103",
"attributes":{"created_at":"2021-04-24T18:55:18+09:00",
"updated_at":"2021-04-24T18:55:18+09:00",
"user":1,
"group_title":null,
"transactions":[{"user":1,
"transaction_journal_id":103,
"type":"withdrawal",
"date":"2021-03-26T00:00:00+09:00",
"order":0,
"currency_id":16,
"currency_code":"JPY",
"currency_name":"Japanese yen",
…

レスポンスbodyが返ってきてますのでOKそうです。(改行編集済み)

というワケで一通り流した後の画面がこちら。

f:id:paloma69:20210425122836p:plain

編集してますが、Firefly iiiの画面です。
項目を書くだけで集計してくれて非常に便利です。
デザインもシンプルながらカッコいいです。

今回流したものはちゃんとページ下部に追加されてます。

まとめ

というわけでクレジットの明細をFirefly iiiに一本化することができました。
やってることはpythonでのテキスト変換とshellでのPOSTと簡単なことですが、
あとはFireflyがやってくれるので非常に楽ちんです。
ツールはしばらくshellとpythonでやりますが、不便が出てきたらまた考えます。

月次で回せる様変換コードに日付の自動挿入をつくらないとですね。
Suicaはチャージ分しか明細に乗ってこないので、ここの詳細化もしたいところです。

また、改行はしましたが全体的にコマンドが長くて読みにくくなってしまいました。
すいません。

おまけ

投入データのテンプレートを書くときに遭遇したのですが、波括弧を含んだfornatは括弧を2重にしないとエラーになってしまう様です。

>>> print('{ "price": {} }'.format(100))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: ' "price"'
>>>
>>> print('{{ "price": {} }}'.format(100))
{ "price": 100 }

マネートラッキングをFireflyに一本化する API探す編

家の収支管理にFirefly iiiを使い始めて半年ちょい。 支出のトラッキングでしか使ってませんが自動で集計してくれるのはありがたい。

私のFirefly iiiを使ったキャッシュフロー確認 - paloma blog

Fireflyは現金払いの集計、クレジット払いの集計は自作ツールという使い分けをしていましたが、Fireflyの方が何かと便利なのでこちらに集約して一本化したいと思います。

クレジットの集計ツールは昔作りました。

月次の支払い集計ツールを作る SQL準備編 - paloma blog

クレジットの明細はcsvでダウンロード出来ますが、Fireflyには直接インポートはできないようで個別にインポート用のサーバを用意する必要があるみたいです。

これは面倒だと思っていましたが、どうやらJSONAPIが提供されているようなのでこちらを使ってクレジットの明細をimportできるようにしたいと思います。

まずは公式ドキュメント

https://docs.Firefly-iii.org/Firefly-iii/api/

APIのページしては短いなと思ったら別のリンクがありました。

Firefly III API documentation

Firefly iiiのAPIはswaggerというAPIフレームワークが使われているようです。
Web系の業務はあまり関わりが無いですが、swaggerなんて聞いたことも無く初耳でした。

APIアクセスする方法は2通りあるようです。

OAuth2.0

名前は聞いたことあるけどよくわからなったもの。
認可フレームワークらしいです。
アクセス許可用のトークンを発行してリソースにアクセスできるようになるという仕組みらしいです。

サイトがあるので説明は割愛します。
使うにはいろいろサーバ、アプリが必要らしい。

The OAuth 2.0 Authorization Framework

Personal token

OAuth2.0環境を使わない人向けに個人token発行の機能もあるようです。 家で使うのでわざわざOAuth2.0の環境は作らずこちらを使いましょう。

リクエストヘッダ

APIを叩く際はヘッダを埋め込むと思うのですがフォーマットが書いてない。
これも標準仕様としてあると言うことか。
再度ドキュメントを読みます。

APIドキュメントのサンプルコマンドは以下になっていました。
サンプルは他にもありますが、以下のコマンドはサーバ情報を取得します。

curl -X GET "https://demo.Firefly-iii.org/api/v1/about" -H  "accept: application/vnd.api+json" -H  "Authorization: Bearer トークン(長いので割愛)

必要なヘッダは

  • accept: application/vnd.api+json
  • Authorization: Bearer

で、Authorizationヘッダにトークンを書けばいい様です。
頭についているBearerって何だ?

Bearer認証

The OAuth 2.0 Authorization Framework: Bearer Token Usage(日本語)

OAuth2.0の仕様の1つで署名無しトークンという意味らしいです。
本人確認はせず、このトークンを持っていれば使えるということです。
他の人の手に渡ったら使われてしまうということですね。

他の認証方法もあるんでしょうか。今回は調べませんが、気になります。

ちなみにBearer認証でもOAuthはトークン発効前にクライアントの認証があるし、私の個人トークン発効でも今回の環境では問題なさそうです。

アクセス

本題。
自分のFirefly iiiのトークンを発行したのでお試しでユーザ情報を取ってみます。

ドキュメントの文法そのまま。

masashi@DESKTOP-986MNSO:/mnt/c/Users/tsuru$ curl -X GET http://192.168.0.10/api/v1/users -H "accept: application/vnd.api+json" -H "Authorization: Bearer $(cat 'Firefly token.txt')" | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   578    0   578    0     0  10181      0 --:--:-- --:--:-- --:--:-- 10321
{
  "data": [
    {
      "type": "users",
      "id": "1",
      "attributes": {
        "created_at": "2021-02-06T10:49:23+09:00",
        "updated_at": "2021-02-06T10:49:23+09:00",
        "email": "XXXXXXXX@gmail.com",
        "blocked": false,
        "blocked_code": null,
        "role": "owner"
      },
      "links": {
        "self": "http://192.168.0.10/api/v1/users/1",
        "0": {
          "rel": "self",
          "uri": "/users/1"
        }
      }
    }
  ],
  "meta": {
    "pagination": {
      "total": 1,
      "count": 1,
      "per_page": 50,
      "current_page": 1,
      "total_pages": 1
    }
  },
  "links": {
    "self": "http://192.168.0.10/api/v1/users?&page=1",
    "first": "http://192.168.0.10/api/v1/users?&page=1",
    "last": "http://192.168.0.10/api/v1/users?&page=1"
  }
}

ちゃんと取れてます。
一応伏せましたが、emailの値がユーザ情報でログインに使います。

まとめ

OAuth関連の仕組みを使うのは初めてだったのでドキュメント探しに手こずりましたが、とりあえずAPIは突ける様になりました。
もちろんPOSTもできるので、またcsvからの変換ツールを作らないといけないですね。
shellでやるかpythonでやるか悩みます。