Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

何でもJenkinsに表示させよう

Jenkins使ってますか

みなさんはJenkins使ってますか? 僕は初心者なので全然詳しくないですが、Jenkinsが好きです。業務でも使ってます。

Jenkinsは本体にもすばらしい機能がたくさんありますが、豊富なプラグインによってさらに機能を拡張できます。

このことによって、JenkinsはCIツールの枠を超え、開発におけるポータルサイトのような、プロジェクトの骨格をなすもののような、とても崇高な位置にたどり着いていると思うのです。

Jenkins何に使ってますか?

みなさんはJenkisをどんな用途で使っていますか? 単にビルドの成功失敗を見るためだけに使っているなんて、もったいない!

僕はJenkinsを見れば、明日の天気から星座占い、ニュースからJOJOの名言まですべて見れるようにしたいのです(僕はJOJOに詳しくありませんが、別にシャアの名言でもいいのです)!

なんでもJenkinsでやってしまう!

JenkinsとEmacsがあれば、すべてができてしまう! そんな世界に憧れるのです!

そうした用途の一つとして、ここ数年夏と冬に現れる、電力需給状況があります。 Jenkinsで電力需給状況を確認して、逼迫すればJenkinsを落とす!そんな男らしい使い方をしたいのです。

Jenkinsプラグインを作る

Jenkinsはもちろんプラグインを自作することができます。

Plugin tutorial - 日本語 - Jenkins Wiki

これでHello Worldできます。 蛙本を読むと、もう少し詳しくプラグインの作り方が載ってます。

Jenkins

Jenkins

しかし、ここから先に進むためには、あまりドキュメントがありません。 そこで似たようなプラグインを探します。

東京電力関連のプラグインがありました。

TEPCO Plugin https://wiki.jenkins-ci.org/display/JENKINS/TEPCO+Plugin

TEPCO Electric Power Usage Widget - Jenkins - Jenkins Wiki https://wiki.jenkins-ci.org/display/JENKINS/TEPCO+Electric+Power+Usage+Widget

筆者は関西在住なので、関西電力のプラグインを作ることにします。

関西電力プラグイン

これをそのままパクれば拝借すれば、もしやURLを書き換えるくらいで完成か…!? と思いきや、そうはいきませんでした。

CSVのフォーマットが違うのです。

2012/12/5 21:05 UPDATE,,,
ピーク時供給力(万kW),時間帯,供給力情報更新日,供給力情報更新時刻,原子力,火力,水力,揚水,地熱・太陽光,他社受電,(北海道再掲),(東北再掲),(東京再掲),(中部再掲),(北陸再掲),(中国再掲),(四国再掲),(九州再掲)
2593,17:00〜18:00,12/5,9:30,240,1402,173,363,0,416,0,0,0,0,0,0,0,0

予想最大電力(万kW),時間帯,予想最大電力情報更新日,予想最大電力情報更新時刻
2260,17:00〜18:00,12/5,9:30

使用率(%),予想気温(最低),予想気温(最高),気温実績(最低),気温実績(最高)
87,3,9,*,*

DATE,TIME,当日実績(万kW),予想値(万kW),前日実績(万kW),使用率(%)
2012/12/5,0:00,1679,0,1575,64
2012/12/5,1:00,1625,0,1531,62
2012/12/5,2:00,1637,0,1554,63
2012/12/5,3:00,1616,0,1549,62
2012/12/5,4:00,1572,0,1516,60
2012/12/5,5:00,1563,0,1490,60
2012/12/5,6:00,1718,0,1606,66
2012/12/5,7:00,1870,0,1755,72
2012/12/5,8:00,2043,0,1926,78
2012/12/5,9:00,2145,0,2041,82
2012/12/5,10:00,2149,0,2043,82
2012/12/5,11:00,2155,0,2035,83
2012/12/5,12:00,2028,0,1911,78
2012/12/5,13:00,2111,0,2011,81
2012/12/5,14:00,2100,0,2015,80
2012/12/5,15:00,2086,0,2014,80
2012/12/5,16:00,2164,0,2096,83
2012/12/5,17:00,2231,0,2209,86
2012/12/5,18:00,2195,0,2195,84
2012/12/5,19:00,2127,0,2130,82
2012/12/5,20:00,0,2090,2053,80
2012/12/5,21:00,0,2020,1971,77
2012/12/5,22:00,0,1930,1898,74
2012/12/5,23:00,0,1830,1795,70

翌日のピーク時供給力(万kW),時間帯,供給力情報更新日,供給力情報更新時刻,翌日の原子力,翌日の火力,翌日の水力,翌日の揚水,翌日の地熱・太陽光,翌日の他社受電,(翌日の北海道再掲),(翌日の東北再掲),(翌日の東京再掲),(翌日の中部再掲),(翌日の北陸再掲),(翌日の中国再掲),(翌日の四国再掲),(翌日の九州再掲)
2553,17:00〜18:00,12/5,18:30,240,1402,175,334,0,403,0,0,0,0,0,0,0,0

翌日の予想最大電力(万kW),時間帯,予想最大電力情報更新日,予想最大電力情報更新時刻
2260,17:00〜18:00,12/5,18:30

翌日の使用率(%),予想気温(最低),予想気温(最高),
88,7,9,

節電コメント
,

DATE,TIME,瞬間値(万kW)
2012/12/5,0:00,1728
2012/12/5,0:03,1722
2012/12/5,0:06,1707
(中略)
2012/12/5,23:51,
2012/12/5,23:54,
2012/12/5,23:57,

長いですね。 1つのファイルにさまざまな形式でデータが書かれています。めんどくさいですね。

関西電力プラグインを実装してみる

とりあえず、TEPCO Pluginのマネをして、この形式のCSVを読み取るようにします。

    @Extension
    public static class CsvDownloader extends PeriodicWork {

        private static final String CSV_URL = "http://www.kepco.co.jp/yamasou/juyo1_kansai.csv";

        private static final Pattern HEADER_PATTERN = compile("(\\d{4}/\\d{1,2}/\\d{1,2} \\d{1,2}:\\d{1,2}) UPDATE,,,");

        private static final Pattern PEAK_PATTERN = compile("(\\d+),(\\d{1,2}:\\d{1,2})〜(\\d{1,2}:\\d{1,2}),\\d{1,2}/\\d{1,2},(\\d{1,2}:\\d{1,2}),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+)");

        private static final Pattern FORECAST_PATTERN = compile("(\\d+),(\\d{1,2}:\\d{1,2})〜(\\d{1,2}:\\d{1,2}),\\d{1,2}/\\d{1,2},(\\d{1,2}:\\d{1,2})");

        private static final Pattern HOURLY_DATA_PATTERN = compile("(\\d{4}/\\d{1,2}/\\d{1,2}),(\\d{1,2}:\\d{1,2}),(\\d+),(\\d+),(\\d+),(\\d+)");

        private static final Pattern MOMENTARY_DATA_PATTERN = compile("(\\d{4}/\\d{1,2}/\\d{1,2}),(\\d{1,2}:\\d{1,2}),(\\d+)");

        @Override
        protected void doRun() throws Exception {
            Date lastUpdated = null;
            int peakCapacity = 0;
            int forecastPeakUsage = 0;
            int forecastPeakPeriod = 0;
            DateFormat fmt = new SimpleDateFormat("yyyy/M/d H:m");
            List<UsageCondition> usages = new ArrayList<UsageCondition>();
            UsageCondition current = new UsageCondition();

            for (String line : loadCsv().split("\r?\n")) {

                Matcher m = HEADER_PATTERN.matcher(line);
                if (m.matches()) {
                    lastUpdated = fmt.parse(m.group(1));
                    continue;
                }

                m = PEAK_PATTERN.matcher(line);
                if (m.matches()) {
                    peakCapacity = Integer.parseInt(m.group(1));
                    continue;
                }

                m = FORECAST_PATTERN.matcher(line);
                if (m.matches() && forecastPeakUsage == 0) {
                    forecastPeakUsage = Integer.parseInt(m.group(1));
                    String forecastPeakTime = m.group(2);
                    forecastPeakPeriod = Integer.parseInt(forecastPeakTime.substring(0, forecastPeakTime.indexOf(':')));
                    continue;
                }

                m = HOURLY_DATA_PATTERN.matcher(line);
                if (m.matches()) {
                    UsageCondition u = new UsageCondition();
                    u.setCapacity(peakCapacity);
                    int usage = Integer.parseInt(m.group(3));
                    if (0 < usage) {
                        u.setUsage(usage);
                    } else {
                        u.setForecastUsage(Integer.parseInt(m.group(4)));
                    }
                    u.setForecastPeakUsage(forecastPeakUsage);
                    u.setForecastPeakPeriod(forecastPeakPeriod);
                    u.setUsageUpdated(fmt.format(lastUpdated));
                    Date date = fmt.parse(String.format("%s %s", m.group(1), m.group(2)));
                    Calendar c = Calendar.getInstance();
                    c.setTime(date);
                    u.setYear(c.get(Calendar.YEAR));
                    u.setHour(c.get(Calendar.HOUR_OF_DAY));
                    u.setMonth(c.get(Calendar.MONTH) + 1);
                    u.setDay(c.get(Calendar.DATE));
                    u.setPercentage(Integer.parseInt(m.group(6)));
                    usages.add(u);
                }

                m = MOMENTARY_DATA_PATTERN.matcher(line);
                if (m.matches()) {
                    if (m.group(3).isEmpty()) {
                        break;
                    }
                    current.setUsage(Integer.parseInt(m.group(3)));
                    current.setUsageUpdated(m.group(1) + " " + m.group(2));
                    current.setCapacity(peakCapacity);
                    current.setForecastPeakUsage(forecastPeakUsage);
                    current.setForecastPeakPeriod(forecastPeakPeriod);

                }

            }
            if (lastUpdated != null) {
                for (Widget w : Hudson.getInstance().getWidgets()) {
                    if (w instanceof KepcoWidget) {
                        KepcoWidget kw = (KepcoWidget) w;
                        kw.setCurrent(JSONSerializer.toJSON(current).toString());
                        kw.setToday(JSONSerializer.toJSON(usages).toString());
                    }
                }
            }
        }

    }

PeriodicWorkクラスを継承し、この処理がJenkinsから呼び出されるようにします。

単純に正規表現で当てて、どの内容なのか判断してます。 で、値をオブジェクトに詰めてます。 最後にJenkins(Hudson)オブジェクトからこのウィジェットを取り出し、JSON文字列をセットします。

CSVファイルをダウンロードする処理はこうです。

        protected String loadCsv() {
            InputStream is = null;
            try {
                URL url = new URL(CSV_URL);
                is = ProxyConfiguration.open(url).getInputStream();
                return IOUtils.toString(is, "Shift_JIS");
            } catch (MalformedURLException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            } finally {
                IOUtils.closeQuietly(is);
            }
        }

CSVファイルは文字エンコーディングがShift_JISでした。 Jenkinsにプロキシを設定しているときは、その設定を使いたいので

ProxyConfiguration.open(url).getInputStream();

としてます。

ウィジェット

前の節で、ウィジェットという単語をいきなり出しました。 これはJenkinsの拡張ポイントです。

@Extension
public class KepcoWidget extends Widget {

    private static final Logger logger = Logger.getLogger(KepcoWidget.class.getName());

    private String current;

    private String today;

    public String getCurrent() {
        return current;
    }

    public void setCurrent(String current) {
        this.current = current;
    }

    public String getToday() {
        return today;
    }

    public void setToday(String today) {
        this.today = today;
    }

}

Widgetクラスを継承して、@Extensionアノテーションを付与しておけばいいようです。 これで、CSVをダウンロードして、パースする部分ができました。 あとはビューを書くだけです。

ビュー

TEPCO Electric Power Usage Widgetを参考に、少々書き換えました。

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:pane width="3" title="関西電力 電気使用状況">
  <tr>
    <td colspan="3" align="center" style="padding: 3px 0px;">
      <table cellspacing="0" cellpadding="0" width="180px">
        <tr id="gauge"></tr>
      </table>
    </td>
  </tr>

  <tr>
    <td class="pane" align="right">使用量</td>
    <td class="pane" align="center"><b id="usage">0</b></td>
    <td class="pane" rowspan="2" align="center" valign="center" style="font-size:200%;" id="percentage"></td>
  </tr>
  <tr>
    <td class="pane" align="right">供給能力</td>
    <td class="pane" align="center"><b id="capacity">0</b></td>
  </tr>
  <tr>
    <td class="pane" align="center" colspan="3" id="updatedTime"></td>
  </tr>

  <tr>
    <td colspan="3" align="center" valign="center" style="padding: 1px;">
      <table cellspacing="0" cellpadding="0" height="105px" width="200px">
        <tr>
          <td id="graph" style="position: absolute;padding: 0px;"></td>
        </tr>
      </table>
    </td>
  </tr>
  <script>
  (中略)
  </script>
</l:pane>
</j:jelly>

JenkinsはJellyを使ってビューを書きます。そんなに特殊なことはないんで、慣れればすぐ書けます。 グラフもTEPCO Electric Power Usage Widgetのグラフを書き換えました。がんばってJavaScriptを書いてます。 あんまりにも長い上に読みづらいので、掲載しませんw

スクリーンショット

こんな感じです。

f:id:jyukutyo:20121211214350p:plain

まとめ

今回のプラグインは、まったく自分で作っていないですね。TEPCO PluginとTEPCO Electric Power Usage Widgetの作者に感謝です。 このように、既存のプラグインをベースに少し付け加えれば、簡単にプラグインを作ることができます。

ソースコード

https://github.com/jyukutyo/kepco-pluginにあるので、よかったらどうぞ。