Current section
Files
Jump to
Current section
Files
lib/p1/parser.ex
defmodule P1.Parser do
use Combine
import Combine.Parsers.Base
import Combine.Parsers.Text
@moduledoc """
Understands the P1 format of Smartmeters and translates them to elixir types
"""
# credo:disable-for-this-file Credo.Check.Refactor.PipeChainStart
@doc false
def parse(line) do
case Combine.parse(line, parser()) do
{:error, reason} -> {:error, reason}
result -> {:ok, result}
end
end
@doc false
def parse!(line) do
case Combine.parse(line, parser()) do
{:error, reason} -> raise reason
result -> result
end
end
defp parser do
choice(nil, [
header_parser(),
version_parser(),
timestamp_parser(),
equipment_identifier_parser(),
tariff_indicator_parser(),
total_energy_parser(),
active_power_parser(),
power_failures_parser(),
long_power_failures_parser(),
long_failures_log_parser(),
voltage_swells_parser(),
voltage_sags_parser(),
voltage_parser(),
amperage_parser(),
active_power_phase_parser(),
message_parser(),
message_code_parser(),
mbus_device_type_parser(),
mbus_equipment_identifier_parser(),
mbus_device_measurement_parser()
])
end
# /ISk5MT382-1000
defp header_parser do
map(char("/"), fn _ -> :header end)
|> word_of(~r/\w{3}/)
|> ignore(char("5"))
|> word_of(~r/.+/)
end
# 1-3:0.2.8(50)
defp version_parser do
map(string("1-3:0.2.8"), fn _ -> :version end)
|> parens(word())
end
# 0-0:96.1.1(4B384547303034303436333935353037)
# 0-1:96.1.0(3232323241424344313233343536373839)
defp equipment_identifier_parser do
map(string("0-"), fn _ -> :equipment_identifier end)
|> digit()
|> ignore(string(":96.1.1"))
|> parens(word_of(~r/[0-9a-f]+/i))
end
# 0-0:1.0.0(101209113020W)
defp timestamp_parser do
map(string("0-0:1.0.0"), fn _ -> :timestamp end)
|> parens(map(word_of(~r/\d+[SW]/), &(timestamp(&1))))
end
# 1-0:1.8.1(123456.789*kWh)
# 1-0:1.8.2(123456.789*kWh)
# 1-0:2.8.1(123456.789*kWh)
# 1-0:2.8.2(123456.789*kWh)
defp total_energy_parser do
map(string("1-0:"), fn _ -> :total_energy end)
|> map(digit(), &(direction(&1)))
|> ignore(string(".8."))
|> map(digit(), &(tariff(&1)))
|> parens(unit(float()))
end
# 0-0:96.14.0(0002)
defp tariff_indicator_parser do
map(string("0-0:96.14.0"), fn _ -> :tariff_indicator end)
|> parens(map(integer(), &(tariff(&1))))
end
# 1-0:1.7.0(01.193*kW)
# 1-0:2.7.0(00.000*kW)
defp active_power_parser do
map(string("1-0:"), fn _ -> :active_power end)
|> map(digit(), &(direction(&1)))
|> ignore(string(".7."))
|> ignore(digit())
|> parens(unit(float()))
end
defp event(previous \\ nil) do
previous
|> parens(ts())
|> parens(unit(integer()))
end
# 0-0:96.7.21(00004)
defp power_failures_parser do
map(string("0-0:96.7.21"), fn _ -> :power_failures end)
|> parens(integer())
end
# 0-0:96.7.9(00002)
defp long_power_failures_parser do
map(string("0-0:96.7.9"), fn _ -> :long_power_failures end)
|> parens(integer())
end
# 1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)
defp long_failures_log_parser do
map(string("1-0:99.97.0"), fn _ -> :long_failures_log end)
|> parens(integer())
|> ignore(string("(0-0:96.7.19)"))
|> many(sequence([event()]))
end
# 1-0:32.32.0(00002)
# 1-0:52.32.0(00001)
# 1-0:72.32.0(00000)
defp voltage_sags_parser do
map(string("1-0:"), fn _ -> :voltage_sags end)
|> map(both(digit(), digit(), &(join(&1, &2))), &(phase(&1)))
|> ignore(string(".32.0"))
|> parens(integer())
end
# 1-0:32.36.0(00000)
# 1-0:52.36.0(00003)
# 1-0:72.36.0(00000)
defp voltage_swells_parser do
map(string("1-0:"), fn _ -> :voltage_swells end)
|> map(both(digit(), digit(), &(join(&1, &2))), &(phase(&1)))
|> ignore(string(".36.0"))
|> parens(integer())
end
# 1-0:32.7.0(220.1*V)
# 1-0:52.7.0(220.2*V)
# 1-0:72.7.0(220.3*V)
defp voltage_parser do
map(string("1-0:"), fn _ -> :voltage end)
|> map(both(digit(), digit(), &(join(&1, &2))), &(phase(&1)))
|> ignore(string(".7.0"))
|> parens(unit(float(), string("V")))
end
# 1-0:31.7.0(001*A)
# 1-0:51.7.0(002*A)
# 1-0:71.7.0(003*A)
defp amperage_parser do
map(string("1-0:"), fn _ -> :amperage end)
|> map(both(digit(), digit(), &(join(&1, &2))), &(phase(&1)))
|> ignore(string(".7.0"))
|> parens(unit(integer(), string("A")))
end
# 1-0:21.7.0(01.111*kW)
# 1-0:41.7.0(02.222*kW)
# 1-0:61.7.0(03.333*kW)
# 1-0:22.7.0(04.444*kW)
# 1-0:42.7.0(05.555*kW)
# 1-0:62.7.0(06.666*kW)
defp active_power_phase_parser do
map(string("1-0:"), fn _ -> :active_power end)
|> map(digit(), &(active_power_phase(&1)))
|> map(digit(), &(direction(&1)))
|> ignore(string(".7.0"))
|> parens(unit(float(), string("kW")))
end
defp message_parser do
map(string("0:96.13.0"), fn _ -> :text_message end)
|> parens(map(word_of(~r/[0-9a-f]*/i), &(Hexate.decode(&1))))
end
defp message_code_parser do
map(string("0:96.13.1"), fn _ -> :message_code end)
|> parens(either(integer(), map(string(""), fn _ -> 0 end)))
end
# 0-1:24.1.0(003)
defp mbus_device_type_parser do
map(string("0-"), fn _ -> :mbus_device_type end)
|> digit()
|> ignore(string(":24.1.0"))
|> parens(integer())
end
# 0-1:96.1.0(3232323241424344313233343536373839)
defp mbus_equipment_identifier_parser do
map(string("0-"), fn _ -> :mbus_equipment_identifier end)
|> digit()
|> ignore(string(":96.1.0"))
|> parens(word_of(~r/[0-9a-f]+/i))
end
# 0-1:24.2.1(101209112500W)(12785.123*m3)
defp mbus_device_measurement_parser do
map(string("0-"), fn _ -> :mbus_device_measurement end)
|> digit()
|> ignore(string(":24.2.1"))
|> parens(ts())
|> parens(unit(float()))
end
# Helper functions
defp ts(previous \\ nil) do
previous |> map(word_of(~r/\d+[SW]/), &(timestamp(&1)))
end
defp timestamp(text) do
# as this is only valid in the netherlands, i can use this trick
tz_offset = case String.last(text) do
"S" -> "+01:00"
"W" -> "+02:00"
end
[date | time] = text
|> String.slice(0 .. String.length(text) - 1)
|> String.codepoints
|> Enum.chunk_every(2)
|> Enum.map(&Enum.join/1)
|> Enum.chunk_every(3)
"20" <> Enum.join(date, "-") <> "T" <> Enum.join(hd(time), ":") <> tz_offset
end
defp parens(previous, parser) do
previous |> between(ignore(char("(")), parser, ignore(char(")")))
end
defp unit(parser) do
pair_both(parser, pair_right(ignore(char("*")), word()))
end
defp unit(parser, unit) do
pair_both(parser, pair_right(ignore(char("*")), unit))
end
defp join(a, b), do: Enum.join([a, b]) |> String.to_integer
defp phase(x) do
case x do
l when l in [2, 31, 32] -> :l1
l when l in [4, 51, 52] -> :l2
l when l in [6, 71, 72] -> :l3
_ -> x
end
end
defp active_power_phase(x) do
case x do
2 -> :l1
4 -> :l2
6 -> :l3
end
end
defp direction(x) do
case x do
1 -> :consume
2 -> :produce
_ -> x
end
end
defp tariff(x) do
case x do
1 -> :low
2 -> :normal
_ -> x
end
end
end