電気料金プランをどないして管理するか

この記事は Nature Remo Advent Calendar 2022 の20日目として書きました。

adventar.org

エンジニアの的場です。 先日Remo E/E liteをご使用されているユーザー向けに、電気料金の見える化機能をリリースしました。

prtimes.jp

こちらのバックエンドの開発をしながら、世の中の電気料金色々あるけどこれどうやって管理しよう・・・という気持ちになったので、これについて諸々書いていこうと思います。

電気料金プラン色々

2016年の小売電力自由化以降、世の中はさまざまな電気料金プランで溢れています。 今回はそれらの電気料金プランを大きく4種類に分けて紹介したいと思います。

料金計算には小売事業者が設定するもの以外にも燃料調整費や再エネ賦課金などの従量料金がありますが、これについては割愛します。

固定単価プラン

一番シンプルな料金プランです。 電力使用量当たりの料金単価が 20円/kWh のように決まっています。 基本料金も0円となっている場合がほとんどで、一月分の使用量に対してシンプルに掛け算をするだけで料金計算ができます。 自由化以降に契約数を伸ばしてきた所謂新電力はこのような料金プランを多く提供しており、シンプルさと電力使用量が少ない需要家には他プランよりリーズナブルになる場合が多い点で人気の形式でした

現在は電力卸売価格の高騰により採算を取るのが困難になり、ほとんど見られなくなりました。

段階料金プラン

現在一番ポピュラーなプラン形式です。 自由化以前から提供されていたプラン形式で、一月分の電力使用量を見たときに、使用量によって単価が変動します。 契約アンペアなどに対応して基本料金が設定されている場合がほとんどで、東京電力の場合だとスタンダードプランが該当します。

www.tepco.co.jp

一般的には使用量が増えるほど単価が上がります。 電気以外の世の中のサービスは使うほど安くなっていくというのが自然ですが、電気料金の場合では逆になっています。 電気は生活における必要性が極めて高いため、なるべく多くの需要家が最低限の電気を使えるように使用量が低い場合は単価も低く設定し、この帯域での低利益を一部の使用量が多い需要家から補填するという収益構造になっています。

市場連動プラン

電力卸売市場(JEPX)の卸売価格と連動して料金単価が変動します。 小売事業者目線だと卸売価格が電気料金を上回ることがないため、安定した収益が見込めます。 ただ、卸売価格が高騰した場合は需要家にダイレクトに影響するため、需要逼迫時に料金が高額になるリスクがあります。 卸売市場の価格変動をモニタして、単価が低い時に電力を多く使い、高い時は節電すると言った工夫をすれば他プランより料金を安く抑えることが可能な場合もありますが、なかなかに上級者向けと言えます。

オール電化プラン(時間帯による単価変動)

時間帯によって料金単価が変動します。 合わせて曜日によって変動したり、段階料金と組み合わせたり結構複雑なプランです。 夜間が安い単価に設定されており、この時間帯に給湯器でお湯を沸かすなどすることで、オール電化の場合は他プランに比べて電気料金を抑えられるような仕組みになっています。

シンプルめ

www.tepco.co.jp

複雑め

kepco.jp

電気料金プラン・計算の困った性質

種類が多い

シンプルに数が多いです。 経産省の資料によると、2021年末時点で電力小売事業者の登録数は732者だそうです。

https://www.meti.go.jp/shingikai/enecho/denryoku_gas/denryoku_gas/pdf/044_03_01.pdf

これらの事業者がそれぞれ複数の料金プランを提供しています。 また、基本料金や従量料金(各種単価)の設定は電力会社エリアによって異なります。 電力会社エリアは全国で10エリアあるため、1つの料金プランにつき10個の料金設定を持つ必要があります。 さらに基本料金などは契約アンペアによって別の設定になってたりするので、とにかく定数が多いです。 料金見える化機能で表示できる料金プランを拡充するにあたって、最大の難関がここかなと思っています。

検針日が厄介

検針日が需要家によって違うので、ある月の電気料金を計算したい場合に計算期間が変わってきてしまいます。 料金見える化機能における検針日についてはRemoアプリから設定できますが、これも現状は簡易的なものになっています。 検針日のパターンはエリアごとにバラバラで、それぞれ約20パターンほどあるため、この辺りの管理も料金計算における課題になってきます。

関西電力エリアの場合 https://www.kansai-td.co.jp/consignment/disclosure/pdf/takusouido_2022.pdf

計算が複雑

料金見える化機能では日毎に電気料金を見ることができますが、段階料金プランの料金計算をする場合はその月にどれだけ電力を使用しているか知る必要があるため、その日の使用量からのみでは料金計算ができません。 将来的にはより複雑な料金設定のプランにも対応していくと思いますが、曜日、時間帯、使用量などの組み合わせが多く、複雑なことをやりつつも綺麗なコードと慎重な検算が必要になってきます。

電気料金プランをどないして管理するか

dbに料金設定をjsonで保存して、この料金設定オブジェクトに対して料金計算のためのinterfaceを実装していく、という感じで管理するのがいいのでは、と思っています。 以下のようなjsonがdbのカラムに入っているイメージです。

{
  "形式":"固定単価",
  "料金設定":{
    "従量料金": 25
  }
}
{
  "形式":"段階料金",
  "料金設定":{
    "基本料金":800,
    "従量料金":[
      {"kWh":0,"単価":10},
      {"kWh":120,"単価":20},
      {"kWh":300,"単価":30},
    ]
  }
}

形式フィールドによって入れ込むstructを切り分けていくイメージになります。 個人的に最近は型をいい感じに切り分けるとなるととりあえずgenerics使えないかなという発想になるのですが、この手法に関しては形式のstringを見て動的に型を切り替える必要があるためgenericsは使えません。 type switchによるパワープレイが必要になり、新たな形式structを追加するたびにこのswitch文のメンテが必要になりそうです。 ここに関しては形式structの型定義をインプットにコードの自動生成をする他ないかな、と思っています。 以下、コードのイメージを記載しておきます。

// 料金計算interface sample
type Pricer interface {
    // calc charge funcs
    calc()
}

type Object struct {
    Type string `json:"type"`
    Pricing Pricer `json:"pricing"`
}

var _ sql.Scanner = (*Object)(nil)
func (o *Object) Scan(val interface{}) error {
    return json.Unmarshal(val.([]byte), o)
}

var _ driver.Valuer = Object{}
func (o Object) Value() (driver.Value, error) {
    return json.Marshal(&o)
}

var pricingByType = map[string]Pricer{
    "固定単価": flat{},
    "段階料金": standard{},
}

// json marshal/unmarshal用の一時的なオブジェクト
type shallowObject struct {
    Type string `json:"形式"`
    Pricing map[string]any `json:"料金設定"`
}

// --- 形式ごとの型定義 ---

type meteredRateUsage struct {
    KWFrom int `json:"kWh"`
    UnitPrice float64 `json:"単価"`
}

type meteredRateHour struct {
    HourFrom int `json:"hour_from"`
    UnitPrice float64 `json:"unit_price"`
}

type meteredRateSimple struct {
    UnitPrice float64 `json:"単価"`
}

type flat struct{
    MeteredRates []meteredRateSimple `json:"従量料金"`
}

func (f flat) calc() {
    // 電気料金計算
}
type standard struct {
    Basic        float64       `json:"基本料金"`
    MeteredRates []meteredRateUsage `json:"従量料金"`
}

func (s standard) calc() {
    // 電気料金計算
}

// --- 以下switch文について自動生成できそう ---

var _ json.Unmarshaler = (*Object)(nil)
func (o *Object) UnmarshalJSON(b []byte) error {
    var shallow shallowObject
    if err := json.Unmarshal(b, &shallow); err != nil {
        return err
    }
    o.Type = shallow.Type

    // 一度料金設定だけjson stringに戻す
    bPricing, err := json.Marshal(shallow.Pricing)
    if err != nil {
        return err
    }

    pricing := pricingByType[shallow.Type]
    switch v := pricing.(type) {
    case flat:
        if err := json.Unmarshal(bPricing, &v); err != nil {
            return err
        }
        o.Pricing = v
    case standard:
        if err := json.Unmarshal(bPricing, &v); err != nil {
            return err
        }
        o.Pricing = v
    default:
        panic("invalid type")
    }

    return nil
}

var _ json.Marshaler = (*Object)(nil)
func (o *Object) MarshalJSON() ([]byte, error) {
    var bPricing []byte
    switch v := o.Pricing.(type) {
    case flat:
        _bPricing, err := json.Marshal(&v)
        if err != nil {
            return nil, err
        }
        bPricing = _bPricing
    case standard:
        _bPricing, err := json.Marshal(&v)
        if err != nil {
            return nil, err
        }
        bPricing = _bPricing
    default:
        panic("invalid type")
    }

    // 汎用的に扱えるよう、一度mapにunmarshalする
    var mPricing map[string]any
    if err := json.Unmarshal(bPricing, &mPricing); err != nil {
        return nil, err
    }
    shallow := shallowObject{
        Type: o.Type,
        Pricing: mPricing,
    }
    return json.Marshal(shallow)
}

思いつきでババっと書いた拙いコードですが、もっといい感じに書けるなどあればご教示いただけると幸いです。

早速社内でFBが!

json marshal/unmarshalをシンプルにできそうだしこういうのでもいいかもなあ、という啓示を早速いただきました。

type obj struct{
  Type string
  FlatPricing *Flat `json:"flat,omitempty"`
  StandardPricing *Standard `json:"standard,omitempty"`
  .. キーが並ぶ
}

func (o obj) pricer() Pricer {
  switch (o.Type) {
    case "flat":
      return o.FlatPricing
    case "standard":
      return o.StandardPricing
    ...
  }
}

確かに、となりました。

json.Marshal, json.UnmarshalPricer interfaceのまま引数を渡してもうまいことやってくれないかなと思っていた時期があり、その名残で自分のコードは若干回りくどい感じになってしまっていたみたいです。

終わりに

電気料金プランをどないして管理するかというテーマで書きましたが、最終的にはコードを書いててエンジニアっぽいアプローチのみになってはしまいました。 シンプルに料金プランが多いという点に関して有効な案を考えたつもりではありますが、仕組み上どうしてもそれなりのメンテナンスコストがかかってしまうのがこの先のネックになりそうです。 エンジニア以外のスタッフも扱いやすいインターフェースを用意して社内連携して管理していくのが重要だなと感じています。 業界全体のことを考えると検針日や電気料金計算などが公開APIとして整備されていくような流れになっていくと、料金見える化のような便利な機能がより拡充できてユーザーにも価値を提供できるのかなと考えています。j

仲間募集中

Natureではエンジニア絶賛募集中です。

少しでも興味を持たれた方はカジュアルに面談もできますので、まずは気軽にご応募をしていただければと思います。

herp.careers