lib/grizzly/virtual_devices/thermostat.ex

defmodule Grizzly.VirtualDevices.Thermostat do
  @moduledoc """
  Implementation of a virtual device for a thermostat
  """

  @behaviour Grizzly.VirtualDevices.Device

  alias Grizzly.ZWave.{Command, DeviceClass}

  alias Grizzly.ZWave.Commands.{
    BasicReport,
    ThermostatSetpointReport,
    ThermostatModeReport,
    ThermostatFanModeReport,
    SensorMultilevelReport,
    SensorMultilevelSupportedSensorReport
  }

  @impl Grizzly.VirtualDevices.Device
  def init(_) do
    state = %{
      setpoints: %{
        heating: 22.0,
        cooling: 26.0
      },
      fan_mode: :auto_low,
      temperature: 24.0,
      mode: :cooling,
      basic: :on,
      scale: :celsius
    }

    {:ok, state, DeviceClass.thermostat_hvac()}
  end

  @impl Grizzly.VirtualDevices.Device
  def handle_command(%Command{name: :thermostat_setpoint_get} = command, state) do
    type = Command.param!(command, :type)

    case Map.get(state.setpoints, type) do
      nil ->
        {:noreply, state}

      value ->
        {:ok, report} = ThermostatSetpointReport.new(type: type, value: value, scale: state.scale)
        {:reply, report, state}
    end
  end

  def handle_command(%Command{name: :thermostat_setpoint_set} = command, state) do
    scale = Command.param!(command, :scale)
    type = Command.param!(command, :type)

    value =
      command
      |> Command.param!(:value)
      |> maybe_convert_value(scale, state)

    case Map.get(state.setpoints, type) do
      nil ->
        {:reply, :ack_response, state}

      _old_value ->
        new_setpoints = Map.put(state.setpoints, type, value)
        {:reply, :ack_response, %{state | setpoints: new_setpoints}}
    end
  end

  def handle_command(%Command{name: :thermostat_fan_mode_get}, state) do
    {:ok, report} = ThermostatFanModeReport.new(mode: state.fan_mode)

    {:reply, report, state}
  end

  def handle_command(%Command{name: :thermostat_fan_mode_set} = command, state) do
    mode = Command.param!(command, :mode)

    {:reply, :ack_response, %{state | fan_mode: mode}}
  end

  def handle_command(%Command{name: :sensor_multilevel_get}, state) do
    {:ok, report} =
      SensorMultilevelReport.new(sensor_type: :temperature, scale: 1, value: state.temperature)

    {:reply, report, state}
  end

  def handle_command(%Command{name: :thermostat_mode_get}, state) do
    {:ok, report} = ThermostatModeReport.new(mode: state.mode)
    {:reply, report, state}
  end

  def handle_command(%Command{name: :thermostat_mode_set} = command, state) do
    new_mode = Command.param!(command, :mode)
    {:reply, :ack_response, %{state | mode: new_mode}}
  end

  def handle_command(%Command{name: :sensor_multilevel_supported_sensor_get}, state) do
    {:ok, report} = SensorMultilevelSupportedSensorReport.new(sensor_types: [:temperature])
    {:reply, report, state}
  end

  def handle_command(%Command{name: :basic_get}, state) do
    {:ok, report} = BasicReport.new(value: state.basic)
    {:reply, report, state}
  end

  def handle_command(%Command{name: :basic_set} = command, state) do
    value = Command.param!(command, :value)
    {:reply, :ack_response, %{state | basic: value}}
  end

  def handle_command(_other, state), do: {:noreply, state}

  defp maybe_convert_value(value, scale, state) do
    if state.scale == scale do
      value
    else
      convert_value(value, scale, state.scale)
    end
  end

  defp convert_value(value, :celsius, :fahrenheit) do
    celsius_to_fahrenheit(value)
  end

  defp convert_value(value, _fah, _cel) do
    fahrenheit_to_celsius(value)
  end

  defp celsius_to_fahrenheit(cel) do
    cel * 1.8 + 32
  end

  defp fahrenheit_to_celsius(fah) do
    (fah - 32) / 1.8
  end
end