lib/grizzly/zwave/commands/meter_report.ex

defmodule Grizzly.ZWave.Commands.MeterReport do
  @moduledoc """
  This module implements the command METER_REPORT of the COMMAND_CLASS_METER command class.

  This command is used to advertise the current meter reading at the sending node.

  Params:

    * `:meter_type` - the type of metering physical unit being reported (required)
    * `:scale` - the unit used (required)
    * `:value` - the value being reported (required)

  """

  @behaviour Grizzly.ZWave.Command

  alias Grizzly.ZWave.{Command, DecodeError}
  alias Grizzly.ZWave.CommandClasses.Meter

  @type meter_type :: :electric | :gas | :water | :heating | :cooling
  @type meter_scale :: atom
  @type param ::
          {:meter_type, meter_type} | {:scale, meter_scale} | {:value, number}

  @impl true
  @spec new([param()]) :: {:ok, Command.t()}
  def new(params) do
    command = %Command{
      name: :meter_report,
      command_byte: 0x02,
      command_class: Meter,
      params: params,
      impl: __MODULE__
    }

    {:ok, command}
  end

  @impl true
  def encode_params(command) do
    meter_type = Command.param!(command, :meter_type)
    meter_type_byte = encode_meter_type(meter_type)
    scale_byte = encode_meter_scale(Command.param!(command, :scale), meter_type)
    value = Command.param!(command, :value)
    precision = precision(value)
    int_value = round(value * :math.pow(10, precision))
    byte_size = if int_value == 0, do: 1, else: ceil(:math.log2(int_value + 1) / 8)

    <<meter_type_byte, precision::size(3), scale_byte::size(2), byte_size::size(3),
      int_value::size(byte_size)-unit(8)>>
  end

  @impl true
  @spec decode_params(binary()) :: {:ok, [param()]} | {:error, DecodeError.t()}
  def decode_params(
        <<_::size(3), meter_type_byte::size(5), precision::size(3), scale_byte::size(2),
          size::size(3), int_value::size(size)-unit(8), _::binary>>
      ) do
    with {:ok, meter_type} <- decode_meter_type(meter_type_byte),
         {:ok, scale} <- decode_meter_scale(scale_byte, meter_type) do
      value = int_value / :math.pow(10, precision)
      {:ok, [meter_type: meter_type, scale: scale, value: value]}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end

  defp precision(value) when is_number(value) do
    case String.split("#{value}", ".") do
      [_] -> 0
      [_, dec] -> String.length(dec)
    end
  end

  defp encode_meter_type(:electric), do: 0x01
  defp encode_meter_type(:gas), do: 0x02
  defp encode_meter_type(:water), do: 0x03
  defp encode_meter_type(:heating), do: 0x04
  defp encode_meter_type(:cooling), do: 0x05

  defp encode_meter_scale(:kwh, :electric), do: 0x00
  defp encode_meter_scale(:kvah, :electric), do: 0x01
  defp encode_meter_scale(:w, :electric), do: 0x02
  defp encode_meter_scale(:pulse_count, :electric), do: 0x03
  defp encode_meter_scale(:v, :electric), do: 0x04
  defp encode_meter_scale(:a, :electric), do: 0x05
  defp encode_meter_scale(:power_factor, :electric), do: 0x06
  defp encode_meter_scale(:mst, :electric), do: 0x07
  defp encode_meter_scale(:cubic_meters, :gas), do: 0x00
  defp encode_meter_scale(:cubic_feet, :gas), do: 0x01
  defp encode_meter_scale(:pulse_count, :gas), do: 0x03
  defp encode_meter_scale(:mst, :gas), do: 0x07
  defp encode_meter_scale(:cubic_meters, :water), do: 0x00
  defp encode_meter_scale(:cubic_feet, :water), do: 0x00
  defp encode_meter_scale(:us_gallons, :water), do: 0x02
  defp encode_meter_scale(:pulse_count, :water), do: 0x03
  defp encode_meter_scale(:mst, :water), do: 0x07
  defp encode_meter_scale(:kwh, :heating), do: 0x00
  defp encode_meter_scale(:kwh, :cooling), do: 0x00

  defp decode_meter_type(0x01), do: {:ok, :electric}
  defp decode_meter_type(0x02), do: {:ok, :gas}
  defp decode_meter_type(0x03), do: {:ok, :water}
  defp decode_meter_type(0x04), do: {:ok, :heating}
  defp decode_meter_type(0x05), do: {:ok, :cooling}

  defp decode_meter_type(byte),
    do: {:error, %DecodeError{value: byte, param: :meter_type, command: :meter_report}}

  defp decode_meter_scale(0x00, :electric), do: {:ok, :kwh}
  defp decode_meter_scale(0x01, :electric), do: {:ok, :kvah}
  defp decode_meter_scale(0x02, :electric), do: {:ok, :w}
  defp decode_meter_scale(0x03, :electric), do: {:ok, :pulse_count}
  # defp decode_meter_scale(0x04, :electric), do: {:ok,:v }
  # defp decode_meter_scale(0x05, :electric), do: {:ok, :a}
  # defp decode_meter_scale(0x06, :electric), do: {:ok,:power_factor }
  # defp decode_meter_scale(0x07, :electric), do: {:ok, :mst}
  defp decode_meter_scale(0x00, :gas), do: {:ok, :cubic_meters}
  defp decode_meter_scale(0x01, :gas), do: {:ok, :cubic_feet}
  defp decode_meter_scale(0x03, :gas), do: {:ok, :pulse_count}
  # defp decode_meter_scale(0x07, :gas), do: {:ok, :mst}
  defp decode_meter_scale(0x00, :water), do: {:ok, :cubic_meters}
  defp decode_meter_scale(0x01, :water), do: {:ok, :cubic_feet}
  defp decode_meter_scale(0x02, :water), do: {:ok, :us_gallons}
  defp decode_meter_scale(0x03, :water), do: {:ok, :pulse_count}
  # defp decode_meter_scale(0x07, :water), do: {:ok, :mst}
  defp decode_meter_scale(0x00, :heating), do: {:ok, :kwh}
  defp decode_meter_scale(0x00, :cooling), do: {:ok, :kwh}

  defp decode_meter_scale(byte, _),
    do: {:error, %DecodeError{value: byte, param: :meter_type, command: :meter_scale}}
end