■ Phoenix Channelをテーマにした過去記事
Phoenix Channelとelm-phoenixについて -Qiita
Phoenix Channelで作る最先端Webアプリ - Reactチャット編 - Qiita
Phoenix Channelで作る最先端Webアプリ - topic-subtopic編 - Qiita
東京電力電力供給状況監視 - Phoenix Channel - Qiita
今回はPhoenix channelやElmプログラムの練習として、以下のAPIを利用して、リアルタイムに東京電力の電力供給状況を監視するシステムを作ってみました。phoenixのchannel機能を利用して、Elixir Applicationで定期的に取得した電力供給状況を、Elmクライアントにpushしていくシステムです。Elmクライアントではlineグラフ表示で、リアルタイムな遷移を表現しています。
東京電力電力供給状況API
東京電力電力供給状況監視アプリ ==> Listener同期ボタンをクリックしてね!
http://www.mypress.jp:4000/
(3)Elmクライアント <--> (1)Phoenix Channel <--> (2)Elixir Application
システムの構成としては、まず(1)Phoenix Channel があります。これは(2)Elixir Applicationと(3)Elmクライアントの通信のハブのような役割を果たします。(2)Elixir Applicationは、定期的(5分おき)に東京電力電力のAPIを叩いて電力供給状況を取得しchannelに書き込みます。(3)Elmクライアント は定期的に書き込まれる電力供給状況をリッスンしていて、リアルタイムにグラフを更新していきます。
技術的に必要なことは以下の過去記事にまとめてありますので、詳細が必要な場合に参照して頂ければと思います。
Phoenix Channelで作る最先端Webアプリ - Elixier Application編 - Qiita
<== Elixir Applicationをchannelに接続します
Phoenix Channelとelm-phoenixについて - Qiita
<== Elmクライアントをchannelに接続します
Elm Svg と elm-visualization - Qiita
<== Elmクライアント側にグラフを表示します
東京電力APIからは現在の消費電力(万kW)と供給可能最大電力が取得できます。時間推移をX軸として、消費電力の推移を緑のラインで、供給可能最大電力の推移(ほぼ一定)を赤色のラインで表します。以下のようなグラフになります。お昼休みの時間帯に少しだけ消費電力が落ちているのがわかります。
#1.Phoenix Channel
まずはシステムの大黒柱であるPhoenix Channelの設定を行います。Phoenix Channelは通信のハブであり、基本的な設定は決まった方法で行え、コードも汎用的なものをそのまま使います。
プロジェクトを作成します。
mix phx.new denki_channel --no-ecto
cd denki_channel
Presence機能は使いませんので、手順全体的がシンプルです。
確認だけですが、application.exをチェックします。
#
children = [
supervisor(DenkiChannelWeb.Endpoint, []),
]
#
確認だけですが、endpoint.exをチェックします。
defmodule DenkiChannelWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :denki_channel
socket "/socket", DenkiChannelWeb.UserSocket
#
user_socket.exを修正します。以下のようにchannel行のコメントを外します。
defmodule DenkiChannelWeb.UserSocket do
use Phoenix.Socket
## Channels
channel "room:*", DenkiChannelWeb.RoomChannel
#
def connect(_params, socket) do
{:ok, socket}
end
#
ちなみにここで定義してあるconnect/2は、クライアントからのsocket接続のリクエストが必ず通る関数です。この関数は認証などを行う場所として適していますが、今回はデフォルトのソースのままで修正は加えません
room_channel.exを以下のように作成します。ほぼ定型のコードです。
defmodule DenkiChannelWeb.RoomChannel do
use DenkiChannelWeb, :channel
def join("room:lobby", %{"user_name" => user_name}, socket) do
IO.puts("join ok!")
socket = assign(socket, :user_name, user_name)
{:ok, socket}
end
def join("room:" <> _private_room_id, _params, _socket) do
IO.puts("join NG!")
{:error, %{reason: "unauthorized"}}
end
# event = "new_msg" or "sync_msg", "all_msg"
def handle_in(event, %{"msg" => msg}, socket) do
IO.puts("handle_in: #{event} = #{msg}")
user_name = socket.assigns[:user_name]
broadcast(socket, event, %{msg: msg, user_name: user_name})
{:reply, :ok, socket}
end
def terminate(_reason, socket) do
user_name = socket.assigns[:user_name]
IO.puts("terminate user=#{user_name}")
{:noreply, socket}
end
end
ここでchannelイベントについて確認しておきます。本プログラムでは3つのchannelイベントを使います。new_msgとsync_msg、all_msgです。
Elixir ApplicationはListenerとして東電の電力供給状況を取得したら new_msgイベントを書き込みElmクライアントに伝えます。このイベントをクライアント側で受けとりリアルタイムにグラフを更新します。
(3)Elmクライアント <-- (1)Phoenix Channel <-- (2)Elixir Application
クライアント側がブラウザを開くと、最初はデータが無いのでグラフは表示されません。この場合、Elixir ApplicationがGenServerとして保持している直近のデータを一括して送ってもらうことができます。その意思をElixir Applicationに伝えるために、Elmクライアントはsync_msgイベントを送ります。
(3)Elmクライアント --> (1)Phoenix Channel --> (2)Elixir Application
Elixir Applicationがsync_msgイベントを受け取ると、stateに保持してあるデータを一つのメッセージに加工して、all_msgイベントという形でElmクライアントに返します。
(3)Elmクライアント <-- (1)Phoenix Channel <-- (2)Elixir Application
以上のようなイベントの流れをchannel moduleであるroom_channel.exではハンドリングする必要があります。handle_in(event, %{"msg" => msg}, socket)の第一引数はeventという変数で、event = "new_msg" or "sync_msg", "all_msg"のようなマッチングを想定しています。中身はbroadcastしているだけなので改善の余地があります。
terminate/2は、例えばブラウザを閉じた時にとかのsocket通信がクローズした時に呼ばれます。
以上でChannelの設定が終了です。
#2.Elixir Application
次にElixir Applicationを構築していきます。
mix new denki_listen --sup
cd denki_listen
ElixirでChannelのクライアント機能を使うためにphoenixchannelclientを、http通信で東京電力のAPIを叩くためにhttpoisonをインストールします。またElixirで日時を扱うための定番であるtimexもインストールします。
defp deps do
[
{:phoenixchannelclient, "~> 0.1.0"},
{:httpoison, "~> 1.0"},
{:timex, "~> 3.1"}
]
end
以下のコマンドでインストールできます。
mix deps.get
確認だけですが、DenkiListen.ApplicationがこのApplicationのスタート地点であることを確認しておきます。
#
def application do
[
extra_applications: [:logger],
mod: {DenkiListen.Application, []}
]
end
#
application.exにおいてchldrenを追加しておきます。
#
children = [
DenkiListen.Listener
]
#
Elixir Applicationのworkerであるlistener.exを定義します。以下に概略を説明します。
defmodule DenkiListen.Listener do
use GenServer #child_spec
use Timex
@denki_url "http://tepco-usage-api.appspot.com/quick.txt"
@messages_length 48
@work_interval 300_000
#@work_interval 60_000
# (1)公開関数 start_link - プロセスの起動
def start_link(_args) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
# (2)公開関数 reset_timer - timerをリセットする
def reset_timer() do
GenServer.call(__MODULE__, :reset_timer)
end
# (1)start_linkのcallback - state=%{}で初期化
def init(state) do
_timer = Process.send_after(self(), :start, 10_000)
{ :ok, state }
end
# (2)reset_timerのcallback
def handle_call(:reset_timer, _from, state) do
:timer.cancel(state.timer)
timer = Process.send_after(self(), :work, @work_interval)
{:reply, :ok, %{ state | timer: timer}}
end
# スタート時にsocketやchannelの初期化を行います。
def handle_info(:start, _state) do
channel =
get_pid()
|> get_socket()
|> get_channel()
if channel == :error do
IO.puts("channel=error")
{ :reply, %{}, %{} }
else
res_chan = PhoenixChannelClient.join(channel)
channel_check(res_chan, channel)
end
end
# 定期的に東電APIのデータをnew_msgイベントとして送信します。
def handle_info(:work, state) do
IO.puts("handl-info: work")
# Do my work
messages = get_denki(state)
# Start the timer again
timer = Process.send_after(self(), :work, @work_interval)
{:noreply, %{ state | timer: timer, messages: messages}}
end
# クライアントからのsync_msgイベントを受け、stateの全データをall_msgイベントで送信します。
def handle_info({"sync_msg", %{"user_name" => "anonymous"}}, state) do
IO.puts("handl-info: new_msg = sync")
push_messages(state)
{:noreply, state}
end
# (重要):work 以外の予期しないメッセージを受け止め、出力する
def handle_info(work, state) do
IO.puts("handl-info: other")
IO.inspect work
#IO.inspect state
{:noreply, state}
end
defp channel_check( {:ok, _}, channel ) do
t = Process.send_after(self(), :work, @work_interval)
init_state = %{ timer: t, channel: channel, messages: [] }
{ :noreply, init_state }
end
defp channel_check( error, _channel ) do
IO.puts("join error: ???")
IO.inspect(error)
{ :noreply, %{} }
end
defp get_pid do
res_pid = PhoenixChannelClient.start_link()
case res_pid do
{:ok, pid} -> pid
_ -> :error
end
end
defp get_socket (:error) do
IO.puts("pid=error")
:error
end
defp get_socket (pid) do
res_socket = PhoenixChannelClient.connect(pid,
host: "localhost",
port: 4000,
path: "/socket/websocket",
secure: false,
heartbeat_interval: 30_000)
case res_socket do
{:ok, socket} -> socket
_ -> :error
end
end
defp get_channel(:error) do
IO.puts("socket=error")
:error
end
defp get_channel(socket) do
channel = PhoenixChannelClient.channel(socket, "room:lobby", %{user_name: "listener"})
channel
end
defp get_denki(state) do
# Call API & Persist
IO.puts "To the moon!"
case HTTPoison.get(@denki_url) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} -> update_messages(state, body)
{:ok, %HTTPoison.Response{status_code: 404}} -> state.messages
{:error, %HTTPoison.Error{reason: _reason}} -> state.messages
end
end
defp update_messages( %{channel: c, messages: m}, msg ) when length(m) > @messages_length do
updated_msg = update_time(msg)
PhoenixChannelClient.push(c, "new_msg", %{msg: updated_msg})
[ updated_msg ] ++ List.delete_at(m, -1) # -1でlastを削除。新しい順
end
defp update_messages( state, msg ) do
updated_msg = update_time(msg)
PhoenixChannelClient.push(state.channel, "new_msg", %{msg: updated_msg})
[ updated_msg ] ++ state.messages # 新しい順
end
defp update_time( msg ) do
# 面倒なのでAPIで取得した時間は無視し、サーバの現在時間(-5分)を使う
ut = Timex.now("Asia/Tokyo")
|> Timex.shift(minutes: -5)
|> Timex.to_unix
case String.split(msg, ",") do
[ _, d1, d2 ] -> to_string(ut) <> "000" <> "," <> d1 <> "," <> d2
_ -> msg
end
end
defp push_messages( %{ messages: [] } ) do
IO.puts("push_messages = empty!!!")
end
defp push_messages( state ) do
# reduceで古い順番になる
msg = ( Enum.reduce state.messages, "", (fn (m, ms)-> "\n" <> m <> ms end) )
|> String.trim
IO.puts("push_messages = #{msg}")
PhoenixChannelClient.push(state.channel, "all_msg", %{msg: msg})
end
end
まずDenkiListen.Listenerが起動されると、init関数が呼ばれます。ここでは10秒後にhandle_info(:start, _state)を呼ぶようにしています。handle_info(:start, _state)ではsocket接続の初期化やchannelへのjoinなどを行います。サーバ側の準備を待つために10秒遅らせています。
def init(state) do
_timer = Process.send_after(self(), :start, 10_000)
{ :ok, state }
end
今度はhandle_info(:start, _state)の中で無事channelへのjoinが行われた時に、以下のようにしてhandle_info(:work, state)を呼んでいます。5分後に呼ばれます。これが定期処理のスタート地点です。
defp channel_check( {:ok, _}, channel ) do
t = Process.send_after(self(), :work, @work_interval)
#
end
handle_info(:work, state)では東電のAPIを叩いて現在の電力供給状況を取得し、channelに書き込んでいます。また取得したデータはGenServerのstateに保持するようにしています。ここで注目してほしいのが以下の再帰callです。Process.send_afterで5分後にもう一度自分自身を呼ぶようにセットしています。このようにしてGenServerで定期的な処理を実現しています。
def handle_info(:work, state) do
#
timer = Process.send_after(self(), :work, @work_interval)
#
end
以下のhandle_infoはElmクライアントからsync_msgイベントが送られた時の処理です。GenServerのstateに保持した電力供給状況の履歴をchannelに一気に書き込みます。Elmクライアントはこのデータでグラフを再描画します。ブラウザ立ち上げ時に初期化する目的で作りました。あまりchannelの利用に詳しくないのでもう少し上手な実現方法があるかとは思います。
def handle_info({"sync_msg", %{"user_name" => "anonymous"}}, state) do
IO.puts("handl-info: new_msg = sync")
push_messages(state)
{:noreply, state}
end
最後に少しダーティな部分の説明です。東電から取得するデータの形式は、「HH:MM,XXXX,YYYY」です。HH:MMは時刻(24時間制)、XXXXは電力消費量(万kW)、YYYYは供給可能電力(万kW)です。クライアントではHH:MMはunixtime(マイクロ秒)が都合が良いわけです。ここでは手抜きですが、HH:MMをパースして変換するのが面倒なので、単に現在時間(-5分)をデータの時間として置き換えます。unixtimeを文字列変換し末尾に"000" を連結し、マイクロ秒としています。update_time関数でこの処理を行っています。
defp update_time( msg ) do
# 面倒なのでAPIで取得した時間は無視し、サーバの現在時間(-5分)を使う
ut = Timex.now("Asia/Tokyo")
|> Timex.shift(minutes: -5)
|> Timex.to_unix
case String.split(msg, ",") do
[ _, d1, d2 ] -> to_string(ut) <> "000" <> "," <> d1 <> "," <> d2
_ -> msg
end
end
以上がElixir Applicationの大まかな流れとなります。
#3.Dependencyとしてdenki_listen Applicationを追加する
さてPhoenix Channelのdenki_channelに戻ります。
上で構築したdenki_listen ApplicationをDependencyとしてdenki_channelに組み込みます。全く独立したApplicationですが、Elixirでは以下のように簡単に追加できます。
mix.exsのdepsにdenki_listenと相対パスを追加します。
#
defp deps do
[
{:phoenix, "~> 1.3.0"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_html, "~> 2.10"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
{:denki_listen, path: "../denki_listen"}
]
end
#
次のコマンドでインストールします。これでdenki_channel Applicationからdenki_listen Applicationの全機能を利用できることができます。しかもdenki_channelを起動すると、自動的にdenki_listenも起動されます。
mix deps.get
起動は以下のコマンドですね。但し実際はElmクライアントを作成してから起動することにします。
mix phx.server
or
iex -S mix phx.server
#4.Elmクライアント設定
さて最後にElmクライアントを設定していきます。
cd assets
brunchの設定を変更します。変更点は(A)と(B)です。
#
paths: {
// Dependencies and current project directories to watch
//---------- (A)"elm"を追加
watched: ["static", "css", "js", "elm", "vendor"],
//----------
// Where to compile files to
public: "../priv/static"
},
// Configure your plugins
plugins: {
babel: {
// Do not use ES6 compiler in vendor code
ignore: [/vendor/]
},
//---------- (B)elmBrunch項を追加
elmBrunch: {
elmFolder: "elm",
mainModules: ["MyChat.elm"],
outputFolder: "../vendor"
}
//----------
},
#
(B)のElm拡張をを扱えるように、elm-brunchをインストールします。
npm install --save-dev elm-brunch
次にelmディレクトリを作成し必要なパッケージをインストールします。グラフを表示するためelm-visualizationを使います。
mkdir elm
cd elm
elm-package install elm-lang/html
elm-package install elm-lang/websocket
elm-package install lovasoa/elm-csv
elm-package install gampleman/elm-visualization
elm-phoenixをインストールするために、elm-package.jsonを修正します。
{
...
"dependencies": {
...
"saschatimme/elm-phoenix": "0.3.0 <= v < 1.0.0",
...
}
...
}
elm-github-installのインストールについて過去記事を参照してください。Phoenix Channelとelm-phoenixについて - Qiita
以下のコマンドでelm-phoenixをインストールします。
elm-github-install
この時exampleもインストールされ、cssファイルなどが勝手にロードされてしまうので、削除します。
rm -rf ./assets/elm/elm-stuff/packages/saschatimme/elm-phoenix/0.3.2/example/
以下にElmのコードを掲載します。ポイントだけ説明します。
module MyChat exposing (..)
import Csv
import Json.Encode aselm JE
import Json.Decode as JD exposing (Decoder)
import Html exposing (Html)
import Html.Attributes as Attr
import Html.Events as Events
import Phoenix
import Phoenix.Channel as Channel exposing (Channel)
import Phoenix.Socket as Socket exposing (Socket)
import Phoenix.Push as Push
import Date exposing (Date)
import Svg exposing (..)
import Svg.Attributes exposing (..)
import Visualization.Axis as Axis exposing (defaultOptions)
import Visualization.List as List
import Visualization.Scale as Scale exposing (ContinuousScale, ContinuousTimeScale)
import Visualization.Shape as Shape
--maxY = 4000.0
--minY = 2500.0
maxY = 4200.0
minY = 2500.0
main : Program Never Model Msg
main =
Html.program
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
elmUserName : String
elmUserName =
"anonymous"
-- MODEL
type alias Model =
{ denki1 : List (Date, Float)
, denki2 : List (Date, Float)
}
type alias Message
= ( String, String )
initModel : Model
initModel =
{ denki1 = []
, denki2 = []
}
init : ( Model, Cmd Msg )
init =
( initModel, Cmd.none )
-- UPDATE
type Msg
= NewMsg JD.Value
| AllMsg JD.Value
| SyncMsg
update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
case message of
NewMsg payload ->
case Debug.log "NewMsg" JD.decodeValue decodeNewMsg payload of
Ok msg ->
(modelUpdate model msg) ! []
Err err ->
model ! []
AllMsg payload ->
case Debug.log "AllMsg" JD.decodeValue decodeNewMsg payload of
Ok msg ->
(modelUpdate model msg) ! []
Err err ->
model ! []
SyncMsg ->
let
push =
Push.init "room:lobby" "sync_msg"
|> Push.withPayload (JE.object [ ( "msg", JE.string "dummy message" ) ])
in
model ! [ Phoenix.push lobbySocket push ]
-- model update functions
modelUpdate : Model -> Message -> Model
modelUpdate model msg =
let
(userName, denkiMess) = Debug.log "msgxxx:" msg
in
case Csv.split(denkiMess) of
[ head ] -> modelSingleUpdate model head
lst -> modelAllUpdate model lst
modelSingleUpdate : Model -> List String -> Model
modelSingleUpdate model lst = case lst of
[t, d1, d2] ->
let
denki1 = List.append model.denki1 [(getDate(t), getFloat(d1))]
denki2 = List.append model.denki2 [(getDate(t), getFloat(d2))]
in
{ model | denki1=denki1, denki2=denki2 }
_ -> model
modelAllUpdate : Model -> List (List String) -> Model
modelAllUpdate model lst =
let
denki1 = List.map getD1 lst
denki2 = List.map getD2 lst
in
{ model | denki1=denki1, denki2=denki2 }
getFloat : String -> Float
getFloat s =
case String.toFloat s of
(Ok f) -> f
_ -> 0.0
getDate : String -> Date
getDate s =
case String.toFloat s of
(Ok f) -> Date.fromTime f
_ -> Date.fromTime 1448928000000
getD1 n = case n of
[t,d1,_] -> (getDate(t), getFloat(d1))
_ -> (getDate("0"), 0.0)
getD2 n = case n of
[t,_,d2] -> (getDate(t), getFloat(d2))
_ -> (getDate("0"), 0.0)
-- Decoder
decodeNewMsg : Decoder (String, String)
decodeNewMsg =
JD.map2 (\userName msg -> ( userName, msg ) )
(JD.field "user_name" JD.string)
(JD.field "msg" JD.string)
-- SUBSCRIPTIONS
lobbySocket : String
lobbySocket =
"ws://www.mypress.jp:4000/socket/websocket"
{-| Initialize a socket with the default heartbeat intervall of 30 seconds
-}
socket : Socket Msg
socket =
Socket.init lobbySocket
lobby : String -> Channel Msg
lobby userName =
Channel.init "room:lobby"
|> Channel.withPayload (JE.object [ ( "user_name", JE.string userName ) ])
|> Channel.on "new_msg" (\msg -> NewMsg msg)
|> Channel.on "all_msg" (\msg -> AllMsg msg)
|> Channel.withDebug
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch [ phoenixSubscription model ]
phoenixSubscription model =
Phoenix.connect socket [ lobby elmUserName ]
--
-- VIEW
view : Model -> Html Msg
view model =
case model.denki1 of
a::b::_ -> Html.div [] [ viewButton, viewSvg model, chatMessages model ]
_ -> Html.div [] [ viewButton, chatMessages model ]
viewButton : Html Msg
viewButton =
Html.div [] [ Html.button [ Events.onClick SyncMsg ] [ text "Listener同期" ] ]
viewSvg : Model -> Svg Msg
viewSvg model =
svg [ width (toString w ++ "px"), height (toString h ++ "px") ]
[ g [ transform ("translate(" ++ toString (padding - 1) ++ ", " ++ toString (h - padding) ++ ")") ]
[ (xAxis model) ]
, g [ transform ("translate(" ++ toString (padding - 1) ++ ", " ++ toString padding ++ ")") ]
[ yAxis ]
, g [ transform ("translate(" ++ toString padding ++ ", " ++ toString padding ++ ")"), class "series" ]
[ Svg.path [ line model model.denki1, stroke "green", strokeWidth "1px", fill "none" ] [],
Svg.path [ line model model.denki2, stroke "red", strokeWidth "1px", fill "none" ] []
]
]
w : Float
w =
900
h : Float
h =
450
padding : Float
padding =
50
xScale : Model -> ContinuousTimeScale
xScale model =
let
x1 = get1st(model.denki1)
x9 = get1st(List.reverse model.denki1)
in
Scale.time ( x1, x9 ) ( 0, w - 2 * padding )
get1st : List (Date, Float) -> Date
get1st lst =
case lst of
(x,_)::_ -> x
_ -> getDate("0")
yScale : ContinuousScale
yScale =
Scale.linear ( minY, maxY ) ( h - 2 * padding, 0 )
xAxis : Model -> Svg Msg
xAxis model =
let
tcount = Basics.min (List.length model.denki1) 6
in
Axis.axis { defaultOptions | orientation = Axis.Bottom, tickCount = tcount } (xScale model)
yAxis : Svg Msg
yAxis =
Axis.axis { defaultOptions | orientation = Axis.Left, tickCount = 20 } yScale
transformToLineData : Model -> ( Date, Float ) -> Maybe ( Float, Float )
transformToLineData model ( x, y ) =
Just ( Scale.convert (xScale model) x, Scale.convert yScale y )
line : Model -> List (Date, Float) -> Svg.Attribute Msg
line model lst =
List.map (transformToLineData model) lst
|> Shape.line Shape.monotoneInXCurve
|> d
--------------
chatMessages : Model -> Html Msg
chatMessages model =
Html.div []
(List.map chatMessage model.denki1)
chatMessage : (Date, Float) -> Html Msg
chatMessage p =
case p of
(d, f) ->
Html.div []
[ Html.span [] [ Html.text (toString d) ]
, Html.span [] [ Html.text (toString f) ]
]
Elmプログラムを走らせるための場所をindex.html.eexに確保します。
<div id="elm-container"></div>
Elmプログラムをロードしembedします。
import "phoenix_html"
const elmDiv = document.querySelector("#elm-container");
const elmApp = Elm.MyChat.embed(elmDiv);
以下はsubscriptionsの定義の部分で、channelを設定している箇所です。できる限りシンプルな設定にしたつもりです。new_msgイベントとsync_msgイベントをリッスンしていて、それぞれNewMsgメッセージとAllMsgメッセージでupdateを呼びます。
-- SUBSCRIPTIONS
lobbySocket : String
lobbySocket =
"ws://www.mypress.jp:4000/socket/websocket"
{-| Initialize a socket with the default heartbeat intervall of 30 seconds
-}
socket : Socket Msg
socket =
Socket.init lobbySocket
lobby : String -> Channel Msg
lobby userName =
Channel.init "room:lobby"
|> Channel.withPayload (JE.object [ ( "user_name", JE.string userName ) ])
|> Channel.on "new_msg" (\msg -> NewMsg msg)
|> Channel.on "all_msg" (\msg -> AllMsg msg)
|> Channel.withDebug
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch [ phoenixSubscription model ]
phoenixSubscription model =
Phoenix.connect socket [ lobby elmUserName ]
updateのNewMsgメッセージを処理する箇所です。modelを更新します。
update message model =
case message of
NewMsg payload ->
case JD.decodeValue decodeNewMsg payload of
Ok msg ->
(modelUpdate model msg) ! []
Err err ->
model ! []
#
ここでDecode結果の msg は以下のような形になります。"listener"はElixir Applicationのchannelのユーザ名です。1523407637000はunixtime(マイクロ秒)で、3480は現在の電力消費量(万kW)で、4161は供給可能電力(万kW)となります。グラフ描画のためにunixtimeはDate型に変換し、電力量はFloat型に変換します。Elmは型にうるさいので、パースしたりして面倒になることも多いのですが、全体的にみてメリットの方が多いと考えます。
msg=("listener","1523407637000,3480,4161")
Elmクライアントはelm-visualizationでグラフを表示します。X軸を描くときに、xScaleをmodelの関数として定義し、時間の遷移に応じてX軸の定義も変更することにしています。
xScale : Model -> ContinuousTimeScale
xScale model =
let
x1 = get1st(model.denki1)
x9 = get1st(List.reverse model.denki1)
in
Scale.time ( x1, x9 ) ( 0, w - 2 * padding )
またsync_msgイベントを発生させるには以下のボタンをクリックします。
viewButton : Html Msg
viewButton =
Html.div [] [ Html.button [ Events.onClick SyncMsg ] [ text "Listener同期" ] ]
これはSyncMsgメッセージでupdate関数を呼び、Phoenix.pushコマンドでsync_msgイベントを発生させます。
update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
case message of
#
SyncMsg ->
let
push =
Push.init "room:lobby" "sync_msg"
|> Push.withPayload (JE.object [ ( "msg", JE.string "dummy message" ) ])
in
model ! [ Phoenix.push lobbySocket push ]
以上で終わりです。