13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

東京電力電力供給状況監視 - Phoenix Channel

Last updated at Posted at 2018-04-11

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軸として、消費電力の推移を緑のラインで、供給可能最大電力の推移(ほぼ一定)を赤色のラインで表します。以下のようなグラフになります。お昼休みの時間帯に少しだけ消費電力が落ちているのがわかります。

image.png

#1.Phoenix Channel

 まずはシステムの大黒柱であるPhoenix Channelの設定を行います。Phoenix Channelは通信のハブであり、基本的な設定は決まった方法で行え、コードも汎用的なものをそのまま使います。

 プロジェクトを作成します。

mix phx.new denki_channel --no-ecto
cd denki_channel

 Presence機能は使いませんので、手順全体的がシンプルです。

 確認だけですが、application.exをチェックします。

lib/denki_channel/application.ex
#
    children = [
      supervisor(DenkiChannelWeb.Endpoint, []),
    ]
#

確認だけですが、endpoint.exをチェックします。

lib/denki_channel_web/endpoint.ex
defmodule DenkiChannelWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :denki_channel

  socket "/socket", DenkiChannelWeb.UserSocket
#

 user_socket.exを修正します。以下のようにchannel行のコメントを外します。

lib/denki_channel_web/channels/user_socket.ex
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を以下のように作成します。ほぼ定型のコードです。

lib/denki_channel_web/channels/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クライアントに伝えます。このイベントをクライアント側で受けとりリアルタイムにグラフを更新します。

new_msg
   (3)Elmクライアント <-- (1)Phoenix Channel <-- (2)Elixir Application

 クライアント側がブラウザを開くと、最初はデータが無いのでグラフは表示されません。この場合、Elixir ApplicationがGenServerとして保持している直近のデータを一括して送ってもらうことができます。その意思をElixir Applicationに伝えるために、Elmクライアントはsync_msgイベントを送ります。

sync_msg
   (3)Elmクライアント --> (1)Phoenix Channel --> (2)Elixir Application

 Elixir Applicationがsync_msgイベントを受け取ると、stateに保持してあるデータを一つのメッセージに加工して、all_msgイベントという形でElmクライアントに返します。

all_msg
   (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もインストールします。

mix.exs
  defp deps do
    [
      {:phoenixchannelclient, "~> 0.1.0"},
      {:httpoison, "~> 1.0"},
      {:timex, "~> 3.1"}
    ]
  end

 以下のコマンドでインストールできます。

mix deps.get

 確認だけですが、DenkiListen.ApplicationがこのApplicationのスタート地点であることを確認しておきます。

mix.exs
#
  def application do
    [
      extra_applications: [:logger],
      mod: {DenkiListen.Application, []}
    ]
  end
#

application.exにおいてchldrenを追加しておきます。

lib/denki_listen/application.ex
#
    children = [
      DenkiListen.Listener
    ]
#

 Elixir Applicationのworkerであるlistener.exを定義します。以下に概略を説明します。

lib/denki_listen/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秒遅らせています。

init
  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分後に呼ばれます。これが定期処理のスタート地点です。

channel_check
  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で定期的な処理を実現しています。

handle_info
  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の利用に詳しくないのでもう少し上手な実現方法があるかとは思います。

handle_info
  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と相対パスを追加します。

mix.exs
#
  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)です。

assets/brunch-config.js
#
  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を修正します。

assets/elm/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のコードを掲載します。ポイントだけ説明します。

assets/elm/MyChat.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に確保します。

lib/denki_channel_web/templates/page/index.html.eex
<div id="elm-container"></div>

 Elmプログラムをロードしembedします。

assets/js/app.js
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
-- 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
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
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
viewButton : Html Msg
viewButton =
    Html.div [] [ Html.button [ Events.onClick SyncMsg ] [ text "Listener同期" ] ]

 これはSyncMsgメッセージでupdate関数を呼び、Phoenix.pushコマンドでsync_msgイベントを発生させます。

update
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 ]

 以上で終わりです。

13
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?