lib/grizzly/zwave/command_classes.ex

defmodule Grizzly.ZWave.CommandClasses do
  require Logger

  defmodule Generate do
    @moduledoc false

    @mappings [
      {0x00, :no_operation},
      {0x02, :zensor_net},
      {0x20, :basic},
      {0x21, :controller_replication},
      {0x22, :application_status},
      {0x23, :zip},
      {0x24, :security_panel_mode},
      {0x25, :switch_binary},
      {0x26, :switch_multilevel},
      {0x27, :switch_all},
      {0x28, :switch_toggle_binary},
      {0x2A, :chimney_fan},
      {0x2B, :scene_activation},
      {0x2C, :scene_actuator_conf},
      {0x2D, :scene_controller_conf},
      {0x2E, :security_panel_zone},
      {0x2F, :security_panel_zone_sensor},
      {0x30, :sensor_binary},
      {0x31, :sensor_multilevel},
      {0x32, :meter},
      {0x33, :switch_color},
      {0x34, :network_management_inclusion},
      {0x35, :meter_pulse},
      {0x36, :basic_tariff_info},
      {0x37, :hrv_status},
      {0x38, :thermostat_heating},
      {0x39, :hrv_control},
      {0x3A, :dcp_config},
      {0x3B, :dcp_monitor},
      {0x3C, :meter_tbl_config},
      {0x3D, :meter_tbl_monitor},
      {0x3E, :meter_tbl_push},
      {0x3F, :prepayment},
      {0x40, :thermostat_mode},
      {0x41, :prepayment_encapsulation},
      {0x42, :thermostat_operating_state},
      {0x43, :thermostat_setpoint},
      {0x44, :thermostat_fan_mode},
      {0x45, :thermostat_fan_state},
      {0x46, :climate_control_schedule},
      {0x47, :thermostat_setback},
      {0x48, :rate_tbl_config},
      {0x49, :rate_tbl_monitor},
      {0x4A, :tariff_config},
      {0x4B, :tariff_tbl_monitor},
      {0x4C, :door_lock_logging},
      {0x4D, :network_management_basic},
      {0x4E, :schedule_entry_lock},
      {0x4F, :zip_6lowpan},
      {0x50, :basic_window_covering},
      {0x51, :mtp_window_covering},
      {0x52, :network_management_proxy},
      {0x53, :schedule},
      {0x54, :network_management_primary},
      {0x55, :transport_service},
      {0x56, :crc_16_encap},
      {0x57, :application_capability},
      {0x58, :zip_nd},
      {0x59, :association_group_info},
      {0x5A, :device_reset_locally},
      {0x5B, :central_scene},
      {0x5C, :ip_association},
      {0x5D, :antitheft},
      {0x5E, :zwaveplus_info},
      {0x5F, :zip_gateway},
      {0x60, :multi_channel},
      {0x61, :zip_portal},
      {0x62, :door_lock},
      {0x63, :user_code},
      {0x64, :humidity_control_setpoint},
      {0x65, :dmx},
      {0x66, :barrier_operator},
      {0x67, :network_management_installation_maintenance},
      {0x68, :zip_naming},
      {0x69, :mailbox},
      {0x6A, :window_covering},
      {0x6B, :irrigation},
      {0x6C, :supervision},
      {0x6D, :humidity_control_mode},
      {0x6E, :humidity_control_operating_state},
      {0x6F, :entry_control},
      {0x70, :configuration},
      {0x71, :alarm},
      {0x72, :manufacturer_specific},
      {0x73, :powerlevel},
      {0x74, :inclusion_controller},
      {0x75, :protection},
      {0x76, :lock},
      {0x77, :node_naming},
      {0x78, :node_provisioning},
      {0x7A, :firmware_update_md},
      {0x7B, :grouping_name},
      {0x7C, :remote_association_activate},
      {0x7D, :remote_association},
      {0x7E, :antitheft_unlock},
      {0x80, :battery},
      {0x81, :clock},
      {0x82, :hail},
      {0x84, :wake_up},
      {0x85, :association},
      {0x86, :version},
      {0x87, :indicator},
      {0x88, :proprietary},
      {0x89, :language},
      {0x8A, :time},
      {0x8B, :time_parameters},
      {0x8C, :geographic_location},
      {0x8E, :multi_channel_association},
      {0x8F, :multi_cmd},
      {0x90, :energy_production},
      {0x91, :manufacturer_proprietary},
      {0x92, :screen_md},
      {0x93, :screen_attributes},
      {0x94, :simple_av_control},
      {0x95, :av_content_directory_md},
      {0x96, :av_content_renderer_status},
      {0x97, :av_content_search_md},
      {0x98, :security},
      {0x99, :av_tagging_md},
      {0x9A, :ip_configuration},
      {0x9B, :association_command_configuration},
      {0x9C, :sensor_alarm},
      {0x9D, :silence_alarm},
      {0x9E, :sensor_configuration},
      {0x9F, :security_2},
      {0xEF, :mark},
      {0xF0, :non_interoperable}
    ]

    defmacro __before_compile__(_) do
      to_byte =
        for {byte, command_class} <- @mappings do
          quote do
            def to_byte(unquote(command_class)), do: unquote(byte)
          end
        end

      from_byte =
        for {byte, command_class} <- @mappings do
          quote do
            def from_byte(unquote(byte)), do: {:ok, unquote(command_class)}
          end
        end

      quote do
        @type command_class ::
                :zensor_net
                | :basic
                | :controller_replication
                | :application_status
                | :zip
                | :security_panel_mode
                | :switch_binary
                | :switch_multilevel
                | :switch_all
                | :switch_toggle_binary
                | :chimney_fan
                | :scene_activation
                | :scene_actuator_conf
                | :scene_controller_conf
                | :security_panel_zone
                | :security_panel_zone_sensor
                | :sensor_binary
                | :sensor_multilevel
                | :meter
                | :switch_color
                | :network_management_inclusion
                | :meter_pulse
                | :basic_tariff_info
                | :hrv_status
                | :thermostat_heating
                | :hrv_control
                | :dcp_config
                | :dcp_monitor
                | :meter_tbl_config
                | :meter_tbl_monitor
                | :meter_tbl_push
                | :prepayment
                | :thermostat_mode
                | :prepayment_encapsulation
                | :operating_state
                | :thermostat_setpoint
                | :thermostat_fan_mode
                | :thermostat_fan_state
                | :climate_control_schedule
                | :thermostat_setback
                | :rate_tbl_config
                | :rate_tbl_monitor
                | :tariff_config
                | :tariff_tbl_monitor
                | :door_lock_logging
                | :network_management_basic
                | :schedule_entry_lock
                | :zip_6lowpan
                | :basic_window_covering
                | :mtp_window_covering
                | :network_management_proxy
                | :schedule
                | :network_management_primary
                | :transport_service
                | :crc_16_encap
                | :application_capability
                | :zip_nd
                | :association_group_info
                | :device_reset_locally
                | :central_scene
                | :ip_association
                | :antitheft
                | :zwaveplus_info
                | :zip_gateway
                | :zip_portal
                | :door_lock
                | :user_code
                | :humidity_control_setpoint
                | :dmx
                | :barrier_operator
                | :network_management_installation_maintenance
                | :zip_naming
                | :mailbox
                | :window_covering
                | :irrigation
                | :supervision
                | :humidity_control_mode
                | :humidity_control_operating_state
                | :entry_control
                | :configuration
                | :alarm
                | :manufacturer_specific
                | :powerlevel
                | :inclusion_controller
                | :protection
                | :lock
                | :node_naming
                | :node_provisioning
                | :firmware_update_md
                | :grouping_name
                | :remote_association_activate
                | :remote_association
                | :battery
                | :clock
                | :hail
                | :wake_up
                | :association
                | :version
                | :indicator
                | :proprietary
                | :language
                | :time
                | :time_parameters
                | :geographic_location
                | :multi_channel
                | :multi_channel_association
                | :multi_cmd
                | :energy_production
                | :manufacturer_proprietary
                | :screen_md
                | :screen_attributes
                | :simple_av_control
                | :av_content_directory_md
                | :av_content_renderer_status
                | :av_content_search_md
                | :security
                | :av_tagging_md
                | :ip_configuration
                | :association_command_configuration
                | :sensor_alarm
                | :silence_alarm
                | :sensor_configuration
                | :security_2
                | :mark
                | :non_interoperable
                | :no_operation

        @doc """
        Try to parse the byte into a command class
        """
        @spec from_byte(byte()) :: {:ok, command_class()} | {:error, :unsupported_command_class}
        unquote(from_byte)

        def from_byte(byte) do
          Logger.warn("[Grizzly] Unsupported command class from byte #{byte}")
          {:error, :unsupported_command_class}
        end

        @doc """
        Get the byte representation of the command class
        """
        @spec to_byte(command_class()) :: byte()
        unquote(to_byte)
      end
    end
  end

  @before_compile Generate

  @doc """
  Turn the list of command classes into the binary representation outlined in
  the Network-Protocol command class specification.

  TODO: add more details
  """
  @spec command_class_list_to_binary([command_class()]) :: binary()
  def command_class_list_to_binary(command_class_list) do
    non_secure_supported = Keyword.get(command_class_list, :non_secure_supported, [])
    non_secure_controlled = Keyword.get(command_class_list, :non_secure_controlled, [])
    secure_supported = Keyword.get(command_class_list, :secure_supported, [])
    secure_controlled = Keyword.get(command_class_list, :secure_controlled, [])
    non_secure_supported_bin = for cc <- non_secure_supported, into: <<>>, do: <<to_byte(cc)>>
    non_secure_controlled_bin = for cc <- non_secure_controlled, into: <<>>, do: <<to_byte(cc)>>
    secure_supported_bin = for cc <- secure_supported, into: <<>>, do: <<to_byte(cc)>>
    secure_controlled_bin = for cc <- secure_controlled, into: <<>>, do: <<to_byte(cc)>>

    bin =
      non_secure_supported_bin
      |> maybe_concat_command_classes(:non_secure_controlled, non_secure_controlled_bin)
      |> maybe_concat_command_classes(:secure_supported, secure_supported_bin)
      |> maybe_concat_command_classes(:secure_controlled, secure_controlled_bin)

    if bin == <<>> do
      <<0>>
    else
      bin
    end
  end

  @doc """
  Turn the binary representation that is outlined in the Network-Protocol specs
  """
  @spec command_class_list_from_binary(binary()) :: [command_class()]
  def command_class_list_from_binary(binary) do
    binary_list = :erlang.binary_to_list(binary)

    {_, command_classes} =
      Enum.reduce(
        binary_list,
        {:non_secure_supported,
         [
           non_secure_supported: [],
           non_secure_controlled: [],
           secure_supported: [],
           secure_controlled: []
         ]},
        fn
          0xEF, {:non_secure_supported, command_classes} ->
            {:non_secure_controlled, command_classes}

          0xF1, {_, command_classes} ->
            {:secure_supported, command_classes}

          0x00, {_, command_classes} ->
            {:secure_supported, command_classes}

          0xEF, {:secure_supported, command_classes} ->
            {:secure_controlled, command_classes}

          command_class_byte, {security, command_classes}
          when command_class_byte not in [0xF1, 0xEF, 0x00] ->
            # Right now lets fail super hard so we can add support for
            # new command classes quickly
            {:ok, command_class} = from_byte(command_class_byte)
            {security, Keyword.update(command_classes, security, [], &(&1 ++ [command_class]))}
        end
      )

    command_classes
  end

  def maybe_concat_command_classes(binary, _, <<>>), do: binary

  def maybe_concat_command_classes(binary, :non_secure_controlled, ccs_bin),
    do: binary <> <<0xEF, ccs_bin::binary>>

  def maybe_concat_command_classes(binary, :secure_supported, ccs_bin),
    do: binary <> <<0xF1, 0x00, ccs_bin::binary>>

  def maybe_concat_command_classes(binary, :secure_controlled, ccs_bin),
    do: binary <> <<0xEF, ccs_bin::binary>>
end